top of page

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

Shift Quality Editorial
Jul 14
3 min read

Updated: Oct 4

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