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:

CommandWhat it does
nnext — run the current line, stop at the next
sstep — same, but step into function calls
ccontinue — run until the next breakpoint or end
llist — show the surrounding code
p nameprint — show the value of name
pp namepretty-print (nicer for dicts/lists)
wwhere — show the call stack
u / dmove up or down the call stack
qquit the debugger
hhelp

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:

  • n runs the whole call, stops on the next line.
  • s steps 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:

  1. Open the Run panel (Cmd+Shift+D / Ctrl+Shift+D).
  2. Click “create a launch.json file” → select Python.
  3. 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.

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.

Toggle theme (T)