Use breakpoints effectively¶
The question. A bug isn't obvious from the code or the logs and you want to step through a specific piece of it — inspect a variable mid-loop, watch what happens inside a called function, pause only when a certain record triggers the failure. Adding print statements everywhere is slow and noisy.
The answer: drop a breakpoint() call where you want to pause. Python opens pdb; you inspect with p, step with n/s, continue with c, and resume normally. No imports, no magic — and you can disable every breakpoint() in one go with PYTHONBREAKPOINT=0.
(Because pdb is interactive, the canonical answer below just sets up the scenario. To actually step through, copy the code into a .py file and run python -m pdb, or leave the breakpoint() line in and run normally.)
# The minimal shape: a function that fails on certain input, and a commented-out
# breakpoint() you can enable to step through.
def parse_config(text: str) -> dict:
'''Parse simple key=value lines. Fails loudly on malformed lines.'''
config = {}
for line in text.strip().split('\n'):
# Enable the next line to drop into pdb at each line:
# breakpoint()
key, value = line.split('=') # <-- will ValueError on bad input
config[key.strip()] = value.strip()
return config
# Good input — runs fine.
print(parse_config('host=localhost\nport=8080'))
# Bad input — unpacks wrong, raises ValueError.
# Uncomment to see the exception in this notebook; run with `python -m pdb`
# to land in the post-mortem debugger at the failing line.
try:
parse_config('host=localhost\nthis line has no equals sign')
except ValueError as exc:
print(f'caught: {exc}')
# Commands inside pdb (once you've run with -m pdb or enabled breakpoint()):
# p line # print the offending line's value
# p line.split('=') # see that split returns only ['this line...']
# n # step to the next line
# s # step into a function call
# c # continue until the next breakpoint or end
# w # show the call stack ('where')
# q # quit
Variant: conditional and post-mortem debugging¶
Two patterns to know beyond the basic breakpoint().
Conditional breakpoint — for loops where you only care about specific iterations:
for i, item in enumerate(items):
if item['status'] == 'error':
breakpoint() # pause only on error rows
process(item)
Post-mortem — run with python -m pdb script.py. On any unhandled exception, pdb opens at the failing frame with locals intact. Useful when you don't know where the bug is.
$ python -m pdb config_parser.py
> config_parser.py(1)<module>()
-> import logging
(Pdb) c
ValueError: not enough values to unpack (expected 2, got 1)
Uncaught exception. Entering post mortem debugging
> config_parser.py(9)parse_config()
-> key, value = line.split('=')
(Pdb) p line
'this line has no equals sign'
(Pdb) p line.split('=')
['this line has no equals sign']
Variant: essential pdb commands¶
The short list. Everything here works inside any pdb prompt, whether you got there via breakpoint() or python -m pdb.
| Command | What it does |
|---|---|
p expr / pp expr |
Print / pretty-print an expression |
n |
Step over the current line |
s |
Step into a function call |
r |
Continue until the current function returns |
c |
Continue to the next breakpoint or program end |
l / ll |
List source around current line / entire function |
w |
Show the call stack ('where am I?') |
u / d |
Move up / down a frame in the stack |
b N |
Set a breakpoint at line N |
b N, cond |
Set a conditional breakpoint |
cl N |
Clear breakpoint N |
q |
Quit the debugger |
Why this works¶
breakpoint() is a built-in that hands control to whichever debugger the PYTHONBREAKPOINT environment variable names — pdb.set_trace by default, ipdb.set_trace if you prefer that, or nothing at all (PYTHONBREAKPOINT=0 disables every call without editing the code). That makes it ideal for leaving strategically-placed breakpoints in source during active debugging: disable them in CI, enable them locally.
Running with python -m pdb script.py gives you post-mortem debugging for free. If the script raises an unhandled exception, pdb opens at the failing frame with every local variable still alive. Much faster than sprinkling print statements after the fact.
Inside pdb, the commands you'll actually use are: p expr to inspect, n to step over, s to step into, c to continue, w to see the call stack, q to quit. That's the 80 % toolkit — the rest of the command set is useful but rarely needed.
Trade-offs¶
print and logging are still valuable — print for exploratory throwaway work, logging for persistent diagnostics you'll want again. breakpoint() is for the cases where the problem needs a live inspection session: 'why does this variable hold that value at this point?'.
For functions called many times, guard the breakpoint with an if: if item['status'] == 'error': breakpoint(). Much faster than typing c through hundreds of iterations of the normal case.
Remember to remove breakpoint() calls before committing. A grep -rn 'breakpoint()' in a pre-commit hook or CI check is a tiny safeguard with a big payoff — a breakpoint() merged to main will hang any automated invocation that isn't expecting an interactive session.
Related reading¶
- Avoid common logging mistakes — when
logger.exceptionbeats a manual step-through. - Configure logging for a project — logging as the always-on counterpart to interactive debugging.
- pdb commands reference — every command in one place.