Skip to content

How to Debug a Python Script with pdb

When a script prints the wrong number or crashes with a traceback, print() calls only go so far. pdb, Python’s built-in debugger, pauses the program mid-run so you can inspect variables and step through the code line by line until you reach the frame that failed. It ships in the standard library, so there is nothing to install.

Every command below runs the same on macOS, Linux, and Windows. This guide uses one small script with a bug:

stats.py
def average(readings):
    return sum(readings) / len(readings)


def summarize(batches):
    for name, readings in batches.items():
        print(f"{name}: avg {average(readings):.1f}")


summarize({"morning": [21, 22, 20], "evening": []})

Running it crashes on the empty "evening" list:

$ python stats.py
morning: avg 21.0
ZeroDivisionError: division by zero

Tip

On a uv-managed project, prefix each command with uv run (uv run python stats.py, uv run python -m pdb stats.py) so it uses the project’s virtual environment. breakpoint() and every debugger command behave the same.

Pause at a line with breakpoint()

Add a breakpoint() call on the line you want to stop at, then run the script the normal way:

def average(readings):
    breakpoint()
    return sum(readings) / len(readings)
$ python stats.py
> stats.py(2)average()
-> breakpoint()
(Pdb)

The program is paused on the breakpoint() line, and the (Pdb) prompt accepts debugger commands. The built-in breakpoint() function arrived in Python 3.7; on older versions, write import pdb; pdb.set_trace() instead.

Inspect state at the prompt

p evaluates an expression and prints it, pp pretty-prints, and args shows the current function’s arguments:

(Pdb) p readings
[21, 22, 20]
(Pdb) args
readings = [21, 22, 20]
(Pdb) p len(readings)
3

l (list) shows the source around the current line, with -> marking where execution is paused:

(Pdb) l
  1  	def average(readings):
  2  ->	    breakpoint()
  3  	    return sum(readings) / len(readings)

whatis expr prints an expression’s type, and interact opens a full Python REPL against the current frame’s variables. The pdb Reference has the complete command table.

Move through the code

Five commands cover most stepping:

  • n (next): run the current line, stepping over any function it calls
  • s (step): run the current line, stepping into a function call
  • c (continue): resume until the next breakpoint or the end of the program
  • r (return): run until the current function returns
  • unt (until): run until a line past the current one is reached

Continuing past the first hit reveals the bug. The breakpoint() fires again for the "evening" batch, and its readings are empty:

(Pdb) c
morning: avg 21.0
> stats.py(2)average()
-> breakpoint()
(Pdb) p readings
[]
(Pdb) p len(readings)
0

len(readings) is 0, so the next line divides by zero.

Exit pdb

q (quit) stops the debugger and aborts the program. c (continue) resumes the program; with no further breakpoint to hit, it runs to completion. Pressing Ctrl-D at the (Pdb) prompt sends end-of-file and exits the same way as q.

Start pdb without editing the file

To debug code you cannot or would rather not modify, such as a third-party module, run the script under pdb instead of inserting breakpoint():

$ python -m pdb stats.py
> stats.py(1)<module>()
-> def average(readings):
(Pdb)

The debugger starts paused before the first line. Set a breakpoint on a function or line number, then continue to it:

(Pdb) b average
Breakpoint 1 at stats.py:2
(Pdb) c
> stats.py(2)average()
-> return sum(readings) / len(readings)

To run a module the way python -m would, use python -m pdb -m mypackage.cli.

Manage breakpoints from the prompt

pdb numbers each breakpoint as you set it, and those numbers drive the rest of the commands:

(Pdb) b average
Breakpoint 1 at stats.py:2
(Pdb) disable 1
Disabled breakpoint 1 at stats.py:2
(Pdb) enable 1
Enabled breakpoint 1 at stats.py:2
(Pdb) cl 1
Deleted breakpoint 1 at stats.py:2

disable N switches a breakpoint off without losing it, enable N turns it back on, and cl N (clear) deletes it. To pause only when a condition holds, attach it with condition:

(Pdb) b average
Breakpoint 1 at stats.py:2
(Pdb) condition 1 len(readings) == 0
New condition set for breakpoint 1.
(Pdb) c
morning: avg 21.0
> stats.py(2)average()
-> return sum(readings) / len(readings)
(Pdb) p readings
[]

The debugger skips the healthy "morning" batch and stops only on the empty one. tbreak sets a breakpoint that clears itself after the first hit, and ignore N count skips the next count hits.

Debug a crash after it happens

To inspect a crash without setting any breakpoints, run the script under pdb with -c continue. It runs normally and drops into a post-mortem session on the frame that raised:

$ python -m pdb -c continue stats.py
morning: avg 21.0
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> stats.py(2)average()
-> return sum(readings) / len(readings)
(Pdb) p readings
[]

The prompt opens in average() with readings bound to the empty list that caused the ZeroDivisionError. From an interactive session, call pdb.pm() after a crash to reach the same frame:

>>> import pdb; pdb.pm()
> stats.py(2)average()
-> return sum(readings) / len(readings)
(Pdb) p readings
[]

Turn breakpoints off in production

Set PYTHONBREAKPOINT=0 to make every breakpoint() call a no-op, so a stray call can never pause a deployed service:

$ PYTHONBREAKPOINT=0 python stats.py

Pointing the variable at a dotted path routes breakpoint() to a different debugger. Setting PYTHONBREAKPOINT=IPython.terminal.debugger.set_trace opens IPython’s debugger instead of pdb.

Learn More

Last updated on