# debugpy: the Debug Adapter Protocol server behind Python debugging in VS Code

> debugpy is Microsoft's implementation of the Debug Adapter Protocol for Python 3, and it is the layer editors talk to when they attach to a running interpreter. This article covers the CLI and import APIs, the vendored pydevd, the attach-by-PID path, and where the project stops being the right tool.

**microsoft/debugpy** — An implementation of the Debug Adapter Protocol for Python

- Repository: https://github.com/microsoft/debugpy
- Website: https://pypi.org/project/debugpy/
- Stars: 2,483 · Forks: 205
- Language: Python
- License: NOASSERTION
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/microsoft-debugpy

## What debugpy actually is, and who needs it

debugpy is not a debugger UI. It is a server that speaks the Debug Adapter Protocol, a JSON protocol that editors use to talk to language-specific debuggers. The README describes it as "An implementation of the Debug Adapter Protocol for Python 3", and that framing matters: the project's job is to translate DAP requests (set a breakpoint, step, evaluate an expression) into operations on a live Python process.

That means the audience is narrow but specific. If you maintain a DAP client, a language server, or an editor integration, debugpy is the Python side of the contract. If you run Python inside a container or on a remote host and want your local editor to attach, debugpy is what listens on the port. If you want a debugger in a plain terminal, you are not the target user; the standard library already provides pdb.

The repository is not archived, and the last push was on 2026-09-24. The most recent release listed is v1.8.22, tagged 2026-09-15, which is ten days before that push. Releases v1.8.21 and v1.8.20 landed earlier in 2026. The cadence suggests ongoing maintenance rather than a frozen artifact.

## The vendored pydevd tree and why the package is not pure Python

The architecture is easier to understand from the build files than from the README. setup.py imports debugpy._vendored and resolves PYDEVD_ROOT from debugpy._vendored.project_root("pydevd"). The actual breakpoint, tracing and frame-inspection machinery lives in that vendored copy of pydevd, not in debugpy's own modules. debugpy is the protocol front end; pydevd is the engine.

This has a visible consequence. In setup.py there is a comment explaining that "all pydevd native modules are prebuilt and packaged as data", and a custom ExtModules class whose __bool__ returns True so that bdist_wheel treats the package as non-pure on setuptools versions where has_ext_modules is not called correctly. In practice this means wheels are platform-specific even though the top-level code is Python. If you build from source, the native attach binaries are already in the tree; you are not compiling them.

The pyproject.toml confirms the boundary. pyright ignores src/debugpy/_vendored/pydevd entirely, and ruff excludes the same path from linting. Vendoring buys a single installable unit at the cost of a large subtree that the project's own static analysis does not cover.

## Installing debugpy and attaching to a script

The project publishes to PyPI, and the homepage listed in the repository metadata is the PyPI page. A normal install is a single pip command; the README does not document any extra steps or system packages.

```bash
pip install debugpy
```

The README's first CLI example runs a script with debugging enabled but does not block, so the code starts executing immediately and a client can attach later if it wants to.

```bash
python -m debugpy --listen localhost:5678 myfile.py
```

Port 5678 is the value used throughout the README's examples. If you want the process to sit still until your editor connects, add the switch the README names for that purpose. The documentation states that this blocks execution until a client attaches.

```bash
python -m debugpy --listen localhost:5678 --wait-for-client myfile.py
```

The hostname is optional. The README says that when only a port is given, the default interface is 127.0.0.1. Passing 0.0.0.0 makes the adapter listen on all interfaces, and the README adds a warning that this should only be done on secure networks, because anyone who can connect to the port can execute arbitrary code inside the debugged process. Take that warning literally. There is no authentication layer described.

## Attaching to a process by PID, and the subprocess switch

The capability that separates debugpy from a launch-only debugger is injection into a process that is already running. The README gives this form, where the PID is a Python process on the same machine:

```bash
python -m debugpy --listen localhost:5678 --pid 12345
```

According to the README, once that command returns, a debugpy server is running inside the process, as if the process had been launched through -m debugpy in the first place. That is the path you want for a long-running service you cannot restart, or for a worker that has already picked up a job.

Subprocesses are a separate concern. The README shows a configure switch for turning them off:

```bash
python -m debugpy --listen localhost:5678 --pid 12345 --configure-subProcess False
```

The debugger logging section adds a detail that follows from this: when subprocess debugging is enabled, separate log files are created for every subprocess. If you enable logging on a process that forks heavily, expect a directory full of debugpy*.log files rather than one. The README does not describe a retention or rotation policy for those files.

## Using debugpy from inside your code with listen and wait_for_client

The import API mirrors the CLI. You call debugpy.listen() with a (host, port) tuple, or with just a port, in which case the README says the host defaults to "127.0.0.1".

```python
import debugpy
debugpy.listen(("localhost", 5678))
```

Blocking until a client attaches is a separate call, which is useful when the code path you want to inspect runs immediately at import time.

```python
import debugpy
debugpy.listen(5678)
debugpy.wait_for_client()  # blocks execution until client is attached
```

The README also documents support for the standard breakpoint() builtin, and debugpy.breakpoint() as a fallback when another handler has overridden it. The behaviour is conditional: if a client is attached, execution pauses on the calling line; if no client is attached, the call does nothing and the code continues. That conditional semantics is worth remembering, because a breakpoint() left in production code is harmless until someone attaches a debugger, at which point it stops the process.

A newer addition is debugpy.trigger_exception_handler(e), which the README compares in spirit to pdb.post_mortem(). It starts post-mortem debugging of a caught exception. By default, with as_uncaught=True, the stop respects the exception breakpoint configuration in the debugger UI, so if breaking on uncaught exceptions is disabled or filtered out, nothing happens. Pass as_uncaught=False to stop unconditionally. The README is explicit about the trade-off: because the traceback has already been unwound, you can inspect frames and evaluate expressions at the stop, but you cannot step through the code that raised.

## Where debugpy is the wrong choice

The first limitation is in the README itself, in the 0.0.0.0 warning: the debug adapter has no access control that the documentation describes. Exposing the port is equivalent to handing out code execution in that process. That rules out binding to a public interface on any network you do not fully control.

The second is the post-mortem constraint on trigger_exception_handler. It is a stop-and-inspect tool, not a time machine. If you need to understand how the stack got into the failing state, this function cannot help; you needed a breakpoint before the exception.

The third is coverage of the vendored engine. pyright and ruff both exclude src/debugpy/_vendored/pydevd, so the project's own type checking and linting do not reach the code that actually implements tracing. That is a deliberate packaging decision, not an oversight, but it means static guarantees stop at the debugpy layer.

Finally, if your only need is to step through a small script in a terminal, debugpy adds a protocol server and a port between you and the interpreter. pdb does not.

## How debugpy differs from pdb and from using pydevd directly

pdb is the standard library's interactive debugger. It runs in the same terminal as your program and its interface is text. debugpy does not replace pdb's engine role so much as wrap a different one: it exposes debugging over DAP so that a graphical client can drive it, and it supports attaching to a process by PID, which pdb does not do out of the box.

The comparison with pydevd is closer, and the repository makes it concrete. debugpy vendors pydevd under src/debugpy/_vendored/pydevd and resolves it through debugpy._vendored.project_root in setup.py. The README frames the project as a DAP implementation; pydevd is the underlying debugger that debugpy packages and drives. If you consume debugpy, you are consuming pydevd through the DAP contract, with the vendored version pinned by the debugpy release you install. Choosing between them is largely a question of whether you want the protocol layer and the packaging, or the engine on its own terms.

Against pdb, the practical difference is the client. debugpy is what an editor attaches to; pdb is what you type into.

## Logging, licence and the cost of staying current

Debugger internals are opaque when something goes wrong, and the README provides one escape hatch. The --log-to switch writes logs to a directory, and the same effect is available through debugpy.log_to() or by setting the DEBUGPY_LOG_DIR environment variable.

```bash
python -m debugpy --log-to path/to/logs myfile.py
```

The README states that debugpy creates several files named debugpy*.log in that directory, one per component, plus one per subprocess when subprocess debugging is on. There is no documented way to turn logging off selectively per component, and no documented size cap.

The licence needs a closer look than the metadata gives. The repository's licence field reports NOASSERTION, while the README badge links to a LICENSE file and labels it MIT. The build file setup.py carries a Microsoft copyright header and points readers to LICENSE in the project root for the actual terms. Because the package ships a vendored pydevd tree as data, the licence obligations you actually inherit depend on what is in that subtree, and the README does not break that down. Read LICENSE yourself; this is not legal advice.

Upgrade cost is low in the ordinary case: the interface is pip install debugpy, and the CLI and import APIs shown in the README have been stable across the 1.8.x releases listed. The exception is anything that depends on the vendored pydevd version, where a debugpy upgrade can move the engine underneath you.

## Conclusion

Adopt debugpy if you need a DAP server that an editor, a container or a remote host can attach to, or if you want to inject a debugger into an already running Python process by PID. Skip it if you only want an interactive prompt in a terminal; pdb already ships with CPython and needs no adapter. Before committing, check the LICENSE file at the repository root, since the package metadata reports NOASSERTION while the README badge points at MIT, and confirm the vendored pydevd tree under src/debugpy/_vendored is acceptable to your licence policy.

## FAQ

### What is debugpy?

debugpy is an implementation of the Debug Adapter Protocol for Python 3. It runs as a server that a DAP client, such as an editor, connects to in order to debug a Python process.

### How do I install debugpy?

The repository's homepage is the PyPI page, so the standard route is pip install debugpy. The README does not list any additional system packages or post-install steps.

### How do I use debugpy?

Run python -m debugpy --listen localhost:5678 myfile.py to start the adapter and execute immediately, or add --wait-for-client to block until a client attaches. The README uses port 5678 in all of its examples.

### How do I use debugpy in VS Code?

The README does not document VS Code configuration such as launch.json. What it does document is the CLI and import API that a DAP client connects to, for example python -m debugpy --listen localhost:5678 myfile.py or debugpy.listen(("localhost", 5678)).

### How do I use debugpy in PyCharm?

The README does not describe PyCharm setup. It describes the DAP server side only: the --listen switch, the --pid attach form, and the debugpy.listen() import API that a client connects to.

## Sources

- [Issues](https://github.com/microsoft/debugpy/issues)
- [microsoft/debugpy on GitHub](https://github.com/microsoft/debugpy)
- [Project website](https://pypi.org/project/debugpy/)
- [README](https://github.com/microsoft/debugpy/blob/main/README.md)
- [Releases](https://github.com/microsoft/debugpy/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/microsoft-debugpy
