AlgoMaster Logo

pdb Debugger

Low Priority15 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

pdb is the debugger that ships with every Python install. There's nothing to download, nothing to configure, and it works the same on a laptop, a server, and a CI runner. This lesson walks through how to enter pdb, the commands used in most sessions, conditional breakpoints, post-mortem debugging on a crash, and alternatives like ipdb and pudb when the built-in feels too plain.

Three Ways to Enter pdb

There are three common entry points into pdb, and they exist for different reasons.

The modern one is the breakpoint() builtin, added in Python 3.7. Drop it into the line where execution should pause, run the script normally, and execution stops at that line with an interactive (Pdb) prompt:

Run the file, and Python pauses where the breakpoint() sits:

The second entry point is the command-line flag python -m pdb script.py. This runs the whole script under the debugger from the very first line. There is no need to edit the source, which is useful when the file is read-only or when breakpoints need to be set anywhere in the program before it starts running.

The third is the legacy form: import pdb; pdb.set_trace(). Before 3.7, this was the only inline option. It still works and appears in older codebases, but breakpoint() does the same job in less typing and respects the PYTHONBREAKPOINT environment variable, which pdb.set_trace() does not.

breakpoint() looks up the PYTHONBREAKPOINT environment variable and dispatches there. Set PYTHONBREAKPOINT=0 and every breakpoint() in the program becomes a no-op. This is the standard safety net for production and CI, so a stray breakpoint() in a deployed file does not hang the process forever at an invisible prompt.

The Commands That Matter

pdb has around thirty commands. About a dozen cover almost every session. The table below is the working set, sorted by frequency of use.

CommandShortWhat it does
nextnRun the current line. Don't step into function calls.
stepsRun the current line. Step into any function call on it.
continuecResume until the next breakpoint or program end.
listlShow 11 source lines around the current line.
longlistllShow the full source of the current function.
printpPrint the value of an expression.
ppppPretty-print (useful for large dicts and lists).
wherewShow the current call stack.
upuMove one frame up the stack (toward the caller).
downdMove one frame down the stack.
returnrRun until the current function returns.
breakbSet a breakpoint by line, function, or condition.
clearclRemove a breakpoint by number.
argsaPrint the arguments of the current function.
!Run any Python statement in the current scope.
quitqAbort the program.
helphList commands, or h cmd for one command.

A common point of confusion is n versus s. Both run the current line. The difference is what they do with a function call sitting on that line. n (next) treats the call as a single step: it runs the call to completion and stops on the next line in the current function. s (step) enters the function and stops at its first line. The rule of thumb is n when the called function is trusted, s when it is suspect.

A short session on a buggy cart total walks through most of these commands:

Running it pauses at the breakpoint() line:

That last line shows the issue. The function adds when it should multiply, so a single-quantity item accidentally produces the right answer, but a quantity of 2 will not. Stepping in reveals the wrong arithmetic directly.

A few useful habits:

  • w (where) shows the call stack. Use it to figure out who called the current function. u and d walk up and down the stack to inspect the caller's variables.
  • a (args) prints the current function's arguments. Useful after stepping in with s for a quick reminder of what was passed.
  • ! runs any Python in the current scope. !cart.append(("milk", 3.49, 1)) mutates the live cart so debugging can continue without restarting the program.
  • pp pretty-prints. pp orders on a list of fifty order dicts is more readable than p orders.
  • ll (longlist) shows the entire current function rather than 11 lines. Use it when the current location in the function is unclear.

pdb traces every line through sys.settrace, which slows execution when a breakpoint is active in a hot loop. The slowdown only matters inside the debugger session itself, not in normal runs.

Conditional Breakpoints

The break command sets a breakpoint without editing the source. Three forms cover almost every use:

The first form breaks at line 7 every time it runs. The second breaks at the first line of the named function. The third is a conditional breakpoint that only fires when the expression after the comma is true.

Conditional breakpoints turn an intractable bug into a one-step investigation. A loop processes 50,000 orders and crashes on exactly one. Without conditionals, the alternative is stepping through thousands of iterations or flooding stdout with prints. With a conditional, write b orders.py:42, order.id == 4217, run, and the debugger pauses on the exact failing call with all locals still alive.

b with no argument lists every breakpoint currently set. cl 2 clears breakpoint number 2. disable 2 turns it off without removing it, and enable 2 turns it back on.

Commands can be attached to a breakpoint so they run automatically every time it hits. This builds a custom trace without permanent source edits:

Now every call to line_total prints the arguments and continues. The result is a targeted log of specific values, written from inside the debugger, without committing anything to the file.

Watching Expressions

pdb does not have a dedicated "watch" command like a graphical IDE, but two patterns produce the same behavior.

The first is display. Typing display some_expr at the prompt prints that expression every time execution stops. When stepping through a loop where the current total should be printed at each step without typing p total repeatedly:

Use undisplay (with no argument) to clear every watched expression, or undisplay 1 to remove a specific one. display with no argument lists what is currently watched.

The second pattern is the breakpoint-with-commands setup from the previous section. To print a value every time a specific line runs, even when not paused, set a breakpoint there with a commands block that prints and continues. The output streams to the terminal as the program runs.

Neither of these is as visual as a watch panel in an IDE, but both are fast to set up and require no extra tools.

Post-Mortem Debugging

The hardest bugs are the ones that cannot be reproduced on demand. The program crashed, a traceback exists, and the failing frame needs to be inspected. pdb has a post-mortem mode for this case.

Three ways to enter it:

python -m pdb script.py is the most common form. It loads the script and pauses before the first line, allowing breakpoints to be set anywhere with b followed by c to run. If the script then raises an uncaught exception, the prompt lands at (Pdb) inside the failing frame with all local variables still alive and inspectable.

pdb.pm() (short for post-mortem) works on the last exception that left a traceback in sys.last_traceback. The REPL keeps that around after any interactive crash, so the workflow is: run the failing code, see the traceback, call pdb.pm(), inspect the frame.

The sys.excepthook form is a development-only technique. It installs a global handler so any uncaught exception in any script drops straight into the debugger. Convenient on a laptop, but not for shipping. A server with that hook installed would hang the first time something failed.

Post-mortem debugging holds the entire stack of the crashed frame in memory, including every local variable and every reference they hold. For a script this is negligible. For a server processing big datasets, a held traceback can keep gigabytes alive until it is released.

Alternatives

The built-in pdb is intentionally minimal. No syntax highlighting, no tab completion on Python expressions in older versions, no second window for variables. For long debugging sessions, three alternatives are worth knowing about.

ipdb is pdb running on top of IPython. After pip install ipdb, it provides the same commands plus colored output, real tab completion on attributes and methods, history across sessions, and the IPython ? operator for inline help on any object. Enter it with import ipdb; ipdb.set_trace(), or wire PYTHONBREAKPOINT=ipdb.set_trace into the shell so every breakpoint() routes there.

pudb is a curses-based full-screen debugger. After pip install pudb, import pudb; pudb.set_trace() opens a TUI with separate panes for source, variables, breakpoints, and stack. It runs in a regular terminal, which makes it valuable over SSH where a graphical IDE is not available. The keybindings take a few minutes to learn, but for stepping through a long stack of frames it is the most readable terminal option.

web-pdb runs a small HTTP server next to the script and serves the debugger UI in a browser. The use case is narrow: debugging a script running inside a container or on a remote host where an HTTP port can be forwarded but a terminal cannot be attached. pip install web-pdb, then import web_pdb; web_pdb.set_trace(), and open the printed URL in a browser.

ToolInstallBest for
pdbBuilt inQuick checks, any machine, CI
ipdbpip install ipdbDaily use, colors and completion
pudbpip install pudbTUI over SSH, full-screen panes
web-pdbpip install web-pdbContainer or remote host, browser UI
VS CodeEditor featureVariable panes, click-to-set breakpoints
PyCharmEditor featureRemote debugging, large refactors

Python 3.13 also brought a sizable upgrade to the built-in pdb. Tab completion now works on Python expressions, not just commands, so cart. followed by Tab lists the attributes. Multi-line input is supported, so a small block of Python can be pasted and run without stitching it into one line with semicolons. On a recent version, the gap between pdb and ipdb has narrowed.

Common Pitfalls

A few habits separate a useful pdb session from a frustrating one.

Do not ship `breakpoint()` to production. Even with PYTHONBREAKPOINT=0 as a safety net, a stray breakpoint() is noise in a code review and dead weight in the source. Remove them before committing. A pre-commit hook that greps for the word breakpoint( catches the obvious cases.

Do not confuse `pdb` with the test runner. Pytest captures stdout by default, so a breakpoint() inside a test appears to hang with no prompt. The fix is pytest --pdb (which drops into the debugger on the first failure) or pytest -s (which disables stdout capture so an inline breakpoint() works).

Watch the case where `n` looks frozen. n runs the current line and stops on the next one. If the current line is a slow operation (a network call, a database query, a big computation), the prompt comes back only when that line completes. The debugger is not broken; it is waiting on the line it was told to run. c plus a more targeted breakpoint somewhere downstream is sometimes the better option.

Be deliberate when mutating state. The ! operator runs arbitrary Python in the current scope, so !cart.clear() or !balance = 0 alters live state. That is powerful for exploring "what if a different value had been passed?" but also a way to send the program down a path it would never have taken naturally. Use it for exploration, not for permanent fixes.

Be careful with side effects in expressions. p some_call_that_writes_to_a_database() will actually call it and write to the database. Inside a debugger it is easy to forget that "printing" a value can have consequences. Pure expressions are safe. Anything that calls a function with effects is not.

A small note on threads and async code. pdb follows the thread that hit the breakpoint and does not know much about other threads or the event loop. For threaded code, breakpoint() works but the rest of the program keeps running in other threads during the pause, which can change the state being inspected. For asyncio, pdb works inside a coroutine, but stepping across await points can be confusing. For both cases, an IDE debugger with thread-aware panes is usually more comfortable.

Quiz

pdb Debugger Quiz

10 quizzes