top of page

Use a Debugger, Not Print — Debugging Systematically, Part 4

Shawn West
Jul 14
3 min read

Updated: Aug 6

Debugging Systematically · Part 4

Adding print statements works — until you're on the fifth round of adding one, re-running, reading, and adding another. A debugger lets you stop the program mid-flight and inspect anything, without editing code and re-running each time. This walks through actually using one: breakpoints, stepping, conditional breaks, and post-mortem debugging — plus the cases where a print really is the right tool.

Print statements work. Debuggers work better. Inspect any state, any time, without modifying code.

Step 1: Set a Breakpoint (5 min)

Python 3.7+:

def process(data):
    breakpoint()  # execution pauses here
    return data.cleaned()

Run normally. When breakpoint() is hit, drops into the debugger (pdb).

(Pdb) p data
{'foo': 'bar'}
(Pdb) p data.cleaned()
{'foo': 'BAR'}
(Pdb) c   # continue

Step 2: pdb Basics (10 min)

Once in pdb:

n         # next line
s         # step into function
c         # continue
l         # list source around here
p var     # print value
pp var    # pretty-print
w         # where (stack trace)
u / d     # up / down stack frames
q         # quit

Most useful: n, s, c, p, w.

Step 3: ipdb / pdb++ (5 min)

pip install ipdb
# Replace breakpoint() with:
import ipdb; ipdb.set_trace()
# Or set: PYTHONBREAKPOINT=ipdb.set_trace

Better UI: syntax highlighting, tab completion, sticky mode (shows source as you step).

Step 4: IDE Debuggers (10 min)

VS Code, PyCharm, others:

  • Click the line number to set a breakpoint

  • F5 to start debugging

  • F10: step over

  • F11: step into

  • Variables shown in sidebar; no p commands needed

  • Click "Watch" to track expressions

Easier than pdb for complex debugging. Use it.

Step 5: Conditional Breakpoints (10 min)

if user.id == 42:
    breakpoint()

Or in IDE: right-click breakpoint, set condition user.id == 42.

Breaks only when the condition holds. For "the bug only happens for one user," this is invaluable.

Step 6: Post-Mortem (10 min)

When an exception fires:

import pdb

try:
    do_something()
except:
    pdb.post_mortem()

Drops you into the frame where the exception was raised. Inspect every variable as if it were live.

Or:

python -m pdb -c continue myscript.py
# Crashes → drops into pdb at the failure point

Step 7: Remote Debugging (10 min)

For services running in containers / production:

# remote_pdb
pip install remote_pdb

from remote_pdb import RemotePdb
RemotePdb('0.0.0.0', 4444).set_trace()
telnet container 4444

Now you can attach to a running container. Useful for production-debugging (carefully).

VS Code supports remote debugging via debugpy.

Step 8: Logging vs. Debugger (10 min)

Use the debugger for:

  • Local reproduction of bugs

  • Understanding control flow

  • Inspecting complex state

Use logging for:

  • Production issues you can't reproduce

  • Long-running processes you want to observe

  • Multi-process / multi-user systems

Both have their place. Choose deliberately.

Step 9: Watchpoints / Tracing (5 min)

For "when does X change?":

import sys

def trace(frame, event, arg):
    if event == "line" and frame.f_locals.get("x") != last_x:
        print(f"x changed: {frame.f_locals['x']}")
    return trace

sys.settrace(trace)

Or use a debugger's watchpoint (often called "data breakpoint"). Tells you when a variable changes.

Step 10: Print Statements Still Have a Place (5 min)

print(f"DEBUG: user={user}, action={action}")

For:

  • Quick checks where setting up a debugger is overkill

  • Logging in test code

  • "Did this code path actually run?"

Don't commit them. Use logging.debug() if you want to keep them.

What You Just Did

Debugger basics: breakpoints, stepping, post-mortem, conditional, remote, vs. logging. The right tool for the right debugging task.

Common Failure Modes

Always print, never debugger. Slow iteration; modifies code; commits leak.

Always debugger, never log. Can't debug production.

Forgetting to remove debug code. breakpoint() ships to prod; service hangs on calls.

Step through 500 lines. Use breakpoints / conditional / post-mortem.

Treat debugger as "magic." Read its commands; it's just a UI.

Continue the Debugging Systematically path

Part of the Debugging Systematically learning path.

bottom of page