The pdb Debugger

pdb is CPython’s standard interactive source-level debugger, shipped in the standard library. It is not a C program bolted onto the runtime — it is pure Python layered on the interpreter’s own introspection machinery. The interactive command loop and “what should I print” logic live in Lib/pdb.py; the lower-level bookkeeping (breakpoint tables, the trace-event dispatcher, the stop/step state machine) lives in a separate base class Bdb in Lib/bdb.py, “a generic Python debugger base class … [that] takes care of the details of the trace facility” while “a derived class should implement user interaction” (per the bdb docs). Pdb is that derived class — class Pdb(bdb.Bdb, cmd.Cmd). Underneath, every breakpoint and every single-step is realized by an interpreter-level tracing hook. The single most important idea: pdb does not “control” the interpreter from outside; it installs a Python callable that the interpreter calls back on each line/call/return/exception, and from inside that callback pdb decides whether to drop you into a prompt.

This note covers the in-process, deterministic debugger. Its sibling Remote Debugging in CPython covers attaching pdb (and other tools) to a running process you did not instrument in advance — a Python 3.14 feature built on top of the same Pdb/Bdb machinery described here.

Mental Model: A Callback the Interpreter Invokes

The classic mechanism is sys.settrace (see sys.settrace and Tracing Hooks). You register one Python function as the global trace function; the interpreter then calls it on every call event (a new frame), and the function may return a local trace function that is in turn called on line, return, and exception events within that frame. pdb’s global trace function is Bdb.trace_dispatch. On each event it asks two questions: is there a breakpoint that fires here? and am I in the middle of a step/next/until command that should stop here? If either is true, it hands control to Pdb.user_line (or user_call/user_return/user_exception), which prints the current line and starts the (Pdb) command loop — a blocking cmd.Cmd read-eval loop. While you sit at the prompt, the target program’s thread is frozen inside the trace callback; when you type continue or step, pdb sets its internal stop-state and returns from the callback, letting the interpreter resume.

flowchart TD
    subgraph interp["CPython eval loop (target thread)"]
        EX["execute bytecode<br/>line N"] --> EV{"trace event?<br/>(line/call/return/exc)"}
        EV -->|yes| TD["Bdb.trace_dispatch(frame, event, arg)"]
        EV -->|no| EX
    end
    TD --> Q1{"break_here(frame)?<br/>consult Breakpoint tables"}
    TD --> Q2{"stop_here(frame)?<br/>step/next/until state"}
    Q1 -->|hit| UL["Pdb.user_line(frame)"]
    Q2 -->|stop| UL
    Q1 -->|no| RET["return trace_dispatch<br/>resume"]
    Q2 -->|no| RET
    UL --> LOOP["(Pdb) command loop<br/>thread is frozen here"]
    LOOP -->|c / s / n / r / until| SET["set stop-state<br/>set_continue / set_step / set_next ..."]
    SET --> RET
    RET --> EX

Diagram: pdb is driven entirely from inside trace_dispatch, which the interpreter calls on each event. The insight is that the debugger has no separate control thread in the classic model — your program’s own thread runs the debugger callback, blocks at the prompt, and only resumes when a command resets the stop-state and the callback returns. This is why a pdb prompt freezes exactly the thread that hit the breakpoint.

Entering the Debugger

There are several front doors, all of which end in Bdb.set_trace installing the trace function.

breakpoint() — the modern entry point (PEP 553, Python 3.7). Historically you wrote import pdb; pdb.set_trace(). PEP 553 added a builtin: “the built-in breakpoint(), when called with defaults, can be used instead of import pdb; pdb.set_trace()” (per the pdb docs). The point of breakpoint() is indirection: it does not hardcode pdb. It “enters a Python debugger at the point of the call” by invoking sys.breakpointhook(), which is overridable, and whose default implementation “consults a new environment variable called PYTHONBREAKPOINT” (per PEP 553). With PYTHONBREAKPOINT unset or set to the empty string, the default hook uses pdb.set_trace(). Set PYTHONBREAKPOINT=0 and every breakpoint() call in the program becomes a no-op — per PEP 553, “with this value sys.breakpointhook() returns None immediately,” so you can leave breakpoint() calls in code and disable them globally without editing files. Set PYTHONBREAKPOINT=some.importable.callable and the hook “imports the some.importable module and gets the callable object from the resulting module, which it then calls” (PEP 553; bare builtin names without dots such as PYTHONBREAKPOINT=int are also accepted) — this is how IDEs and alternative debuggers (e.g. pudb, ipdb, a remote IDE bridge) hijack breakpoint(). The breakpoint() builtin is verifiable locally: python3 -c "import sys; print(sys.breakpointhook)" prints <built-in function breakpointhook>.

pdb.set_trace(*, commands=None). The direct call. In Python 3.13 the behavior changed so that “set_trace() will enter the debugger immediately, rather than on the next line of code to be executed” (per the pdb docs). Python 3.14 added the commands argument — a list of pdb commands to run automatically on entry, equivalent to typing them at the prompt. A subtle 3.14 change: such “inline” breakpoints “always stop the program at calling frame, ignoring the skip pattern.”

pdb.run(statement), runeval(expr), runcall(func, *args), runctx. Launchers that start the program under the debugger from line one rather than dropping in midway.

Command-line: python -m pdb myscript.py. Runs the script under pdb from the start, and — crucially — re-enters post-mortem debugging if the script dies with an unhandled exception, so you can inspect the dead frames. Python 3.14 also added python -m pdb -p PID to attach to an already-running process; that path is the subject of Remote Debugging in CPython and is not a set_trace call at all.

Post-mortem: pdb.pm() and pdb.post_mortem(t=None). When an exception has already propagated and unwound, there is no live frame to stop in — but the traceback object still references the (now-returned) frames and their locals (see Traceback Objects and Stack Unwinding and Stack Frames and the Frame Stack). post_mortem(t) walks that traceback and lets you inspect each frame’s locals as they were at the point of failure; with no argument it “uses the exception that is currently being handled, or raises ValueError if there isn’t one” (the pdb docs note that support for passing an exception object, not just a traceback, was added in 3.13). pdb.pm() is the convenience form: “Enter post-mortem debugging of the exception found in sys.last_exc” — i.e. the last unhandled exception at the interactive prompt.

How Breakpoints and Stepping Actually Work

The Bdb base class is where the mechanism lives, and it is worth reading the real code (Lib/bdb.py ships with the interpreter — on this 3.14.5 system at /usr/lib64/python3.14/bdb.py).

Breakpoint objects. A bdb.Breakpoint is a plain Python object recording a (file, line) target plus metadata: temporary (delete after first hit), cond (a condition string), funcname, enabled, ignore (a skip count), and hits. The class keeps class-level registries: bpbynumber (a list indexed by breakpoint number) and bplist (a dict keyed by (file, line) whose value is a list of breakpoints, because several breakpoints can sit on one line). A revealing comment in the source admits the design wart: “Keeping state in the class is a mistake — this means you cannot have more than one active Bdb instance.” set_break(filename, lineno, temporary, cond, funcname) first checks the line actually exists via linecache, constructs a Breakpoint, and then — and this is the load-bearing part — walks every frame from the current one outward and sets frame.f_trace = self.trace_dispatch on any frame that could contain the breakpoint. That is how a breakpoint set mid-session begins receiving line events: by attaching the local trace function to the relevant live frames.

Deciding to stop on a breakpoint. On each line event, break_here(frame) canonicalizes the filename, checks whether that file:line is in the breaks table, and if so calls the module function effective(file, line, frame). effective iterates the breakpoints registered at that location and applies the firing logic precisely: skip disabled ones; skip ones whose funcname doesn’t match the current function (via checkfuncname); for an unconditional breakpoint, honor the ignore count (decrement and skip while positive) and otherwise fire; for a conditional breakpoint, eval(b.cond, frame.f_globals, frame.f_locals) and fire only if truthy — and, defensively, if the condition’s evaluation raises, “the most conservative thing is to stop on breakpoint regardless of ignore count,” so a broken condition stops rather than silently swallowing the breakpoint. It also increments b.hits. The return value tells break_here whether a temporary breakpoint should now be deleted.

Stepping. step, next, return, and until are not separate mechanisms — they are different settings of one stop-state computed by _set_stopinfo(stopframe, returnframe, stoplineno). set_step() arranges to stop at the very next line in any frame (step into calls). set_next(frame) pins stopframe to the current frame so calls are stepped over (you only stop again in this frame). set_return(frame) stops when the current frame returns. set_continue() clears the stop-state entirely and, if no breakpoints remain, can even tear the trace function back down for speed. On each event, stop_here(frame) compares the running frame against this stop-state to decide whether to surface the prompt. Because all of this is recomputed inside trace_dispatch, the cost of stepping is the cost of being called on every line — which is exactly the performance problem the new backend addresses.

The settrace vs monitoring Backend (New in 3.14)

The historically painful fact about sys.settrace-based debugging is that once a global trace function is installed, the interpreter pays the per-event dispatch cost everywhere, even in frames with no breakpoints. Python 3.12 introduced sys.monitoring (PEP 669; see sys.monitoring), a lower-overhead event API whose key trick is that a callback can return the sentinel sys.monitoring.DISABLE to say “stop calling me for this code location” — so uninteresting lines turn themselves off and cost nothing on subsequent executions.

Python 3.14 wired this into bdb. The Bdb constructor gained a backend parameter: class bdb.Bdb(skip=None, backend='settrace'), where 'settrace' “has the best backward compatibility” and 'monitoring' “uses the new sys.monitoring that was introduced in Python 3.12, which can be much more efficient because it can disable unused events” (per the bdb docs; Changed in version 3.14: Added the backend parameter). Inside Lib/bdb.py you can see the dual path: a _MonitoringTracer class registers callbacks under sys.monitoring.DEBUGGER_ID, maps monitoring events (PY_START, LINE, PY_RETURN, RAISE, …) onto the old call/line/return/exception trace-function contract, and returns sys.monitoring.DISABLE from its jump/event callbacks where appropriate so the interpreter stops re-firing them. Bdb.start_trace() branches: if a monitoring tracer is configured it calls self.monitoring_tracer.start_trace(self.trace_dispatch), otherwise it falls back to sys.settrace(self.trace_dispatch).

pdb exposes the choice at module level: pdb.set_default_backend(backend) and pdb.get_default_backend() were both Added in version 3.14, accepting 'settrace' or 'monitoring', and Pdb.__init__ grew a backend keyword. There is a subtlety worth getting right (confirmed against Lib/pdb.py at the v3.14.5 tag). The module-level default-backend constant is settrace: _default_backend = 'settrace' (line 335), so a bare programmatic Pdb() whose caller passes no backend (and hasn’t called set_default_backend) uses settrace via bdb.Bdb.__init__(self, …, backend=backend if backend else get_default_backend()) (line 364). But the user-facing CLI entry points override this and hardcode monitoring: the python -m pdb main() path constructs Pdb(mode='cli', backend='monitoring', …) (line 3652) and the inline set_trace/pm paths likewise pass backend='monitoring' (lines 2697, 2712). So in the way most people invoke pdb — python -m pdb script.py or breakpoint() — you already get the efficient monitoring backend in 3.14; it is only the lower-level programmatic Pdb() constructor that still defaults to settrace unless you opt in or change the module default.

Reviewed 2026-06-01

The backend wiring above is now pinned to the v3.14.5 source (_default_backend = 'settrace'; CLI/inline paths hardcode 'monitoring'). What remains genuinely open is only the quantitative overhead gap between the two backends — the docs say monitoring is “much more efficient” but give no numbers, and that gap is workload-dependent. To get a figure, benchmark a stepping-heavy session under each backend on a fixed 3.14.5 build.

Worked Example: Commands and Their Mechanics

A typical interactive session, annotated:

$ python -m pdb -c "until 20" -c continue myscript.py
> /home/me/myscript.py(1)<module>()
-> import sys
(Pdb) b 42                 # set_break(file, 42): build Breakpoint, attach f_trace to frames
Breakpoint 1 at /home/me/myscript.py:42
(Pdb) b 50, x > 3          # conditional breakpoint: cond="x > 3" stored on the Breakpoint
Breakpoint 2 at /home/me/myscript.py:50
(Pdb) condition 1 n == 99  # attach a condition to an existing breakpoint by number
(Pdb) ignore 1 5           # set Breakpoint(1).ignore = 5: skip its first 5 hits
(Pdb) c                    # set_continue(): clear stop-state, resume until a breakpoint fires
> /home/me/myscript.py(42)compute()
-> total = a + b
(Pdb) p a                  # evaluate `a` in frame.f_locals/f_globals and print repr
7
(Pdb) pp big_dict          # pretty-print: same evaluation through pprint.pformat
{'k1': 1, 'k2': 2, ...}
(Pdb) n                    # set_next(frame): step OVER calls, stop at next line in this frame
> /home/me/myscript.py(43)compute()
-> result = helper(total)
(Pdb) s                    # set_step(): step INTO — stop at first line of helper()
--Call--
> /home/me/myscript.py(10)helper()
(Pdb) r                    # set_return(frame): run until helper() returns
--Return--
> /home/me/myscript.py(13)helper()->84
(Pdb) until 60             # run until a line > 60 is reached in this frame
(Pdb) !x = 99              # leading '!' forces a Python statement: mutate a local live
(Pdb) c

Line-by-line, the load-bearing points: b 42 runs set_break, which both records a Breakpoint and attaches trace_dispatch to live frames so the breakpoint takes effect immediately; b 50, x > 3 stores the text x > 3 as cond, later eval’d against the frame’s namespaces in effective; ignore 1 5 sets the ignore counter that effective decrements; p/pp both evaluate an arbitrary expression in the current frame’s globals and locals (the difference is only pprint formatting); n vs s vs r are the three _set_stopinfo shapes described above; and the ! prefix is required only when a statement would otherwise be parsed as a pdb command — !continue = 3 assigns to a variable named continue rather than running the continue command. The -c flags on the command line inject commands as if typed, which is how you script “run to line 20, then keep going.”

Failure Modes and Common Misunderstandings

pdb freezes the wrong thing / nothing in async code. The classic backend stops the thread that is inside the trace callback. In asyncio (see The asyncio Event Loop) a naive set_trace() blocks the event loop thread while you sit at the prompt, stalling every other coroutine. Python 3.14 added Pdb.set_trace_async() specifically for awaiting inside the debugger; the regular set_trace still freezes the loop.

Conditions that have side effects, or that raise. cond is eval’d on every arrival at the line. If your condition mutates state, you have changed the program you are debugging. If it raises, pdb conservatively stops anyway (per effective), which surprises people expecting the breakpoint to be skipped.

“My breakpoint never fires.” Common causes: the file path didn’t canonicalize to the same string (symlinks, relative vs absolute), the line has no executable bytecode (a blank line or a pure comment), or you set it via function name on a line that is a def rather than the function’s first executable line — checkfuncname deliberately won’t fire on the def line itself.

Overhead under the default backend. With settrace, merely being in pdb taxes the whole program. A continue past a breakpoint in a hot loop can be dramatically slower than the un-debugged program because every line still dispatches into Python. The monitoring backend mitigates this, but is not the default — see the section above.

State leaks across instances. Because Breakpoint keeps class-level registries (acknowledged as “a mistake” in the source), two Bdb/Pdb instances in one process share breakpoint numbering and tables, which can produce confusing behavior in tools that nest debuggers.

Alternatives and When to Choose Them

pdb is the deterministic, interactive, in-process debugger: you stop the program and poke at live state. It is the right tool when you can run the code yourself and want to step through logic or inspect locals at a known failure. It is the wrong tool when (a) you must not pause the program — for a production service, a stop-the-world prompt is unacceptable, which is the niche of statistical sampling tools like py-spy that read stacks without stopping the program; (b) you want aggregate timing rather than a single inspection — that is cProfile’s job, itself built on the trace/monitoring machinery; or (c) the process is already running and was never instrumented — historically impossible for pdb, now addressed by Remote Debugging in CPython via python -m pdb -p PID. Third-party front ends (ipdb, pudb, IDE debuggers) typically reuse bdb or sys.monitoring underneath and differ only in UI; many install themselves via PYTHONBREAKPOINT or sys.breakpointhook rather than reimplementing the engine.

Production Notes

For a production-shaped process you almost never want a blocking (Pdb) prompt on the request thread. The standard guidance is to gate breakpoint() behind PYTHONBREAKPOINT so it is a no-op in production by default, and to prefer post-mortem (pdb.pm() / post_mortem) over live stepping for incident analysis — you let the exception propagate, capture it, and inspect the dead frames offline. For genuinely live production attachment, the 3.14 remote-attach path (python -m pdb -p PID, built on sys.remote_exec) is the supported mechanism and supersedes the old, unsafe practice of injecting a debugger via signal handlers or gdb-driven PyRun_SimpleString — see Remote Debugging in CPython for the safety model. Note the documented latency caveat that carries over to plain pdb attachment: a process “blocked in a system call or waiting for I/O will only work once the next bytecode instruction is executed or when the process receives a signal,” because stopping happens at interpreter safe points, not arbitrary instants.

See Also