Why this matters
Print debugging means editing the code, rerunning it, reading the output, and repeating, each time guessing in advance which values you will need. A debugger lets you pause a running program at any line and look around: inspect every variable, evaluate expressions, step through the code one line at a time, and walk up the call stack, all without editing anything. Python's built-in debugger, pdb, runs in any terminal, on any machine you can SSH into, with nothing to install. Ten minutes learning its handful of commands pays off for the rest of your career.
How a debugger works
CPython lets a program install a trace function that the interpreter calls on events: each new line, each function call and return, each exception (sys.settrace underneath; Python 3.12+ also has the faster sys.monitoring). pdb installs one, and when the current file and line match a breakpoint, or you asked to step, it pauses the program and reads commands from you. The program is frozen mid-flight, with its whole call stack and every frame's local variables still in memory, which is why you can inspect them.
Starting the debugger
From your code, with breakpoint(). Put this line where you want to stop:
def average(scores):
total = sum(scores)
breakpoint()
return total / len(scores)
print(average([80, 90, 100]))
$ python3 avg.py
> /home/ada/avg.py(4)average()
-> return total / len(scores)
(Pdb) p total
270
(Pdb) p len(scores), scores
(3, [80, 90, 100])
(Pdb) c
90.0
The > line shows the file, line number and function; -> shows the line about to run (it has not run yet). (Pdb) is the prompt. breakpoint() is a built-in (Python 3.7+) that calls pdb.set_trace() by default. Setting the environment variable PYTHONBREAKPOINT=0 disables every breakpoint() call, a safety net if you forget one. Still, remove them before committing.
From the command line, without editing the code.
$ python3 -m pdb avg.py
> /home/ada/avg.py(1)<module>()
-> def average(scores):
(Pdb)
The program stops before its first line, and you set breakpoints from there.
The essential commands
Most commands have a one-letter abbreviation.
| Command | Does |
|---|---|
l (list) / ll | show source around the current line / the whole current function |
n (next) | run the current line, stepping over function calls |
s (step) | run the current line, stepping into a function it calls |
c (continue) | run until the next breakpoint (or the end) |
r (return) | run until the current function returns |
unt N (until) | run until line N in the current frame (handy to finish a loop) |
b N / b func / b file.py:N | set a breakpoint at a line or function |
b N, condition | conditional breakpoint: stop only when the condition is true |
b / cl N | list breakpoints / clear breakpoint number N |
p expr / pp expr | print (pretty-print) the value of an expression |
display expr | show expr's value every time the program stops, if it changed |
w (where) | show the call stack, current frame marked with > |
u / d (up, down) | move to the calling frame / back down, to inspect its variables |
a (args) | arguments of the current function |
interact | open a full Python REPL in the current scope (Ctrl-D to return) |
q (quit) | abort the program |
Pressing Enter on an empty line repeats the last command, so stepping is n, Enter, Enter, Enter. Anything that is not a command is run as Python, so you can type total * 2 or even total = 0 to change a variable and see what happens. When a variable name collides with a command (a variable named n or c), prefix with !: !n evaluates the variable, p n also works.
next versus step
The difference matters constantly:
-> total = compute_total(items)
n: run compute_total completely, stop at the next line here
s: stop at the first line inside compute_total
Use n to walk through code you trust and s to dive into a call you suspect. If you stepped into something uninteresting, r runs to its end and stops as it returns.
Conditional breakpoints
Stopping on every iteration of a 10,000-step loop is useless. Stop only when the interesting thing happens:
(Pdb) b 14, qty == 0
Breakpoint 1 at /home/ada/report.py:14
(Pdb) c
> /home/ada/report.py(14)unit_price()
-> return row["total"] / row["qty"]
(Pdb) p row
{'name': 'widget', 'total': 0, 'qty': 0}
Walking the stack
w prints the call stack like a traceback, newest frame at the bottom, with > at the frame you are inspecting:
(Pdb) w
/home/ada/report.py(22)<module>()
-> main()
/home/ada/report.py(18)main()
-> print(format_row(row))
> /home/ada/report.py(14)unit_price()
-> return row["total"] / row["qty"]
(Pdb) u
> /home/ada/report.py(18)main()
-> print(format_row(row))
(Pdb) p row_number
57
u moved to the caller, main, so p now sees main's local variables. The program does not move; you only change which frame you are looking at.
Post-mortem debugging
The best moment to inspect a crash is the moment it happens. Run the script under pdb with an immediate continue:
$ python3 -m pdb -c continue report.py
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> /home/ada/report.py(6)unit_price()
-> return row["total"] / row["qty"]
(Pdb) p row
{'name': 'widget', 'total': 0, 'qty': 0}
pdb catches the uncaught exception and stops in the frame that raised it, with all variables intact. In the REPL, after something has crashed, import pdb; pdb.pm() ("post-mortem") does the same for the most recent exception. In your own code, pdb.post_mortem() inside an except block opens the debugger on the exception being handled.
Scripted sessions
pdb reads commands from stdin like any program, so you can save a session in a file and replay it:
$ printf 'b 7\nc\np v, best\nq\n' > commands.pdb
$ python3 -m pdb buggy.py < commands.pdb
This is useful for reproducing an investigation, and it is how the exercise in this lesson is checked. A file named .pdbrc in your home or current directory is read at startup in the same way, a good place for aliases and favorite settings.
Which tool when
- Traceback: always read first. Often enough by itself.
- print / logging: quick checks, and for programs where pausing is impossible (services, timing-sensitive code, code running on another machine). Logging stays useful in production.
- pdb: when you do not know what to look at yet, when the state is complex, or when each rerun is slow. Exploring live beats guessing in advance.
- Editors like VS Code and PyCharm put a graphical front end on the same machinery: click to set breakpoints, hover to see values. The concepts (breakpoints, step over and into, call stack, watch expressions) are identical.
Common mistakes
- Thinking the
->line has already run. It is the next line to execute.pa variable assigned on that line and you see its old value, or aNameError. - Using
severywhere and ending up deep inside the standard library. Usenover code you trust,rto climb out. - A variable named like a command (
c,n,l,p): typing it runs the command. Usep cor!c. - Leaving
breakpoint()in code. A program that hangs waiting for(Pdb)input in production or in a test run is a classic. Search for it before committing, and setPYTHONBREAKPOINT=0in automated environments. - Debugging the wrong file. Breakpoints by line number refer to the file pdb is running;
b other.py:12for another module.
Recap
breakpoint()in code, orpython3 -m pdb script.py, starts pdb; it pauses with the next line shown after->.- Core commands:
n,s,c,r,unt,b N [, cond],p/pp,l/ll,w,u/d,q; Enter repeats; other input is evaluated as Python. - Post-mortem:
python3 -m pdb -c continue script.py, orpdb.pm()in the REPL, stops in the crashing frame with its variables. - pdb reads stdin, so sessions can be scripted with a command file.
Going further: in a terminal, run python3 -m pdb on one of your earlier exercise solutions and step through it with n, s and p. Then read the pdb documentation's "Debugger Commands" section for display, commands (run commands automatically at a breakpoint) and retval.