When a print statement isn’t enough, you reach for a debugger — a tool that pauses your program at a specific line and lets you inspect what’s happening.
Python has one built in (pdb), and your editor has a graphical version that’s usually nicer.
breakpoint() — the one line that starts everything
Anywhere in your code, write:
def buggy_function(items: list[int]) -> int:
total: int = 0
for item in items:
breakpoint() # <-- pauses here
total += item
return total
print(buggy_function([1, 2, 3]))
Run the script normally:
uv run python main.py
The program runs up to breakpoint(), then pauses and gives you an interactive prompt:
> /path/to/main.py(4)buggy_function()
-> total += item
(Pdb)
You’re now inside pdb, Python’s built-in debugger. The arrow -> points at the next line that will run.
pdb commands — the essentials
At the (Pdb) prompt, type a command:
| Command | What it does |
|---|---|
n | next — run the current line, stop at the next |
s | step — same, but step into function calls |
c | continue — run until the next breakpoint or end |
l | list — show the surrounding code |
p name | print — show the value of name |
pp name | pretty-print (nicer for dicts/lists) |
w | where — show the call stack |
u / d | move up or down the call stack |
q | quit the debugger |
h | help |
You can also type any Python expression at the prompt:
(Pdb) total
0
(Pdb) item
1
(Pdb) len(items)
3
This is what makes the debugger powerful — you’re inside the running program and can inspect or modify anything.
Conditional breakpoints
A bare breakpoint() fires every time the line runs. To pause only when a condition is true, wrap it in an if:
for i, item in enumerate(items):
if i == 5:
breakpoint()
process(item)
Or use the debugger’s b (break) command at a (Pdb) prompt:
(Pdb) b main.py:42, count > 100
Sets a breakpoint at line 42 of main.py, but only when count > 100.
Stepping into functions
When the arrow is on a line that calls a function:
nruns the whole call, stops on the next line.ssteps into the function — pauses on its first line.
Use s when you suspect the bug is inside the function. Use n to skip past calls you trust.
Editor debuggers
Pdb works in any terminal — useful when SSHing into a server. For day-to-day work, an editor debugger (VS Code, PyCharm, Cursor) is more pleasant:
- Click in the margin to set/clear breakpoints visually.
- See variables in a side panel.
- Click step / step-into / step-out buttons instead of typing.
The mental model is the same. Once you understand pdb, every editor debugger feels familiar.
Setting up VS Code’s debugger
VS Code can debug Python files directly:
- Open the Run panel (
Cmd+Shift+D/Ctrl+Shift+D). - Click “create a launch.json file” → select Python.
- Press F5 to debug the current file.
For more complex setups (with arguments, env vars, modules), edit launch.json. VS Code’s docs have the full reference.
Print debugging vs debuggers
Often a strategically placed print() finds the bug faster than firing up a debugger. The rule of thumb:
- One or two values — use
print(). - Stepping through logic — use a debugger.
- Finding when something changes — use a debugger with a conditional breakpoint.
Both are tools. Don’t feel like you must use the debugger for everything.
A note on breakpoint() and production
breakpoint() is a built-in function. It calls sys.breakpointhook(), which by default starts pdb. You can configure it to do something else via the PYTHONBREAKPOINT environment variable.
In production, set PYTHONBREAKPOINT=0 to disable any accidental breakpoint() calls. Better still — don’t leave breakpoint() in committed code. Ruff flags it.
What’s next
Debuggers help you pause. Next — logging, which lets you observe a running program without stopping it.