The debugger: stopping the program and looking around
One word, and the program pauses
Put this line anywhere:
breakpoint()Run the script normally. Execution stops there and you get a prompt:
> /home/asha/billing/report.py(42)build()
-> total = sum(row["amount"] for row in rows)
(Pdb)You are now inside the running program, at that line, with every variable alive. This is the standard library's debugger, pdb, and breakpoint() has been the way to enter it since Python 3.7. You do not install anything.
Eight commands do almost everything
l(list) — show the code around where you are.llshows the whole function.p expr— print an expression.pppretty-prints a big structure.n(next) — run the current line and stop at the next one in this function.s(step) — same, but step into a function call.c(continue) — run on until the next breakpoint or the end.w(where) — print the call stack: how you got here.u/d— move up and down that stack, to inspect a caller's variables.q(quit) — stop the program.
At the (Pdb) prompt you can also just type Python. len(rows), rows[0].keys(), type(total) all work, and so does calling a function to see what it returns with the actual data. That is the advantage over print statements: you ask new questions without rerunning the program. On a job that takes four minutes to reach the failure, that difference is the afternoon.
One thing to know: p is a command, and if a variable is called n, s, c or l, typing its name runs the command instead. p n prints the variable.
After the crash, not before
The most useful mode is post-mortem. When a script dies, run it again like this:
python3 -m pdb -c continue script.pyIt runs at full speed, and on the exception it drops you into the debugger at the exact frame where it happened, with everything still in place. u walks up to the caller. You get the values that produced the crash, which a traceback alone does not give you.
In a notebook, %debug in the next cell does the same thing for the cell that just failed.
Breaking only when it matters
A breakpoint inside a loop over 100,000 rows is useless — you would press c all afternoon. Two ways round it.
Guard it in code:
if row["id"] == "ADD-4471":
breakpoint()Or set a conditional breakpoint from the prompt:
(Pdb) b report.py:57, row["amount"] < 0b sets a breakpoint at a file and line, and anything after the comma is a condition. This is how you catch the one bad row in a million.
The editor version
VS Code, PyCharm and Thonny all wrap the same machinery in a window: click in the margin to set a breakpoint, press the debug button, and see variables in a panel rather than typing p. Everything above still applies; the commands are buttons. Thonny is the gentlest, and it draws the call stack as boxes, which makes the earlier lesson on scope much more concrete.
Use whichever you have. The skill is knowing what to ask, not which key to press.
When print is still better
The debugger is not always the right tool, and it is honest to say so.
- A bug that only appears across 10,000 iterations is better found by logging a value each time and looking at the pattern. You cannot see a trend one breakpoint at a time.
- Anything involving timing or concurrency changes behaviour when you stop it. Pausing one thread while others run can make the bug disappear, or create a new one.
- Code running on a server you cannot attach to leaves logging as the only option, which is why the previous lesson matters.
A reasonable habit: logging for what happened across a whole run, the debugger for one specific moment you need to look at closely.
Try this now
Take a script that fails, and instead of adding prints, run python3 -m pdb -c continue yourscript.py. When it drops you at the failure, type w, then u, then print the variables in the calling frame. Two minutes, and it will change how you debug.
The one thing to keep
`breakpoint()` and `python -m pdb -c continue` put you inside the running program at the moment of failure, where you can ask new questions of the live state instead of rerunning with more prints.
Before you move on
A pipeline crashes after four minutes on one row out of 800,000. Which approach gets to the cause fastest?
Pick the one you would defend. Nobody sees your answer.