Print statements catch the simplest bugs but leave you blind once the problem gets complex. pdb is Python's built-in debugger — drops you into an interactive REPL at any point in your code. You can inspect variables, step through line by line, evaluate expressions in the current scope. The skill payoff is enormous; many developers never learn it and pay for it daily.
breakpoint() — the modern entry
Python 3.7+ has the breakpoint() builtin. Drop it anywhere in your code and execution stops there, dropping into pdb. PYTHONBREAKPOINT=ipdb.set_trace as an env var swaps in the richer ipdb debugger if you've installed it. PYTHONBREAKPOINT=0 disables breakpoints entirely (useful in CI).
The pdb commands you'll actually use
n next line. s step into a function call. c continue (until next breakpoint or end). p expr print an expression. l list source around current line. w show the call stack. u/d move up/down the stack. q quit. The first three are 80% of usage.
post-mortem — debugging an exception
When code crashed, you can debug from the crash point. python -m pdb script.py runs the script under pdb. From inside an interactive session, import pdb; pdb.pm() after an exception drops into the frame where it happened. Variables are still in scope — you can inspect what the function actually saw.
Pythonic Way: When a bug isn't immediately obvious, drop a breakpoint() and look at the actual values. It's faster than adding print statements you'll have to remove. IDE debuggers (VS Code, PyCharm) wrap pdb with a UI; the underlying skill is the same.
Code
breakpoint() — the entry·python
def buggy(items):
total = 0
for x in items:
breakpoint() # drops to pdb here
total += x * 2
return total
# buggy([1, 2, 3])
# (Pdb) p x — inspect current x
# 1
# (Pdb) p total
# 0
# (Pdb) n — next line
# (Pdb) c — continue to next breakpoint or end
Common pdb commands·python
# Inside pdb prompt:
#
# n next line (skip into-function calls)
# s step (into function calls)
# c continue (until next breakpoint)
# p expr print expression
# pp expr pretty-print
# l list source around current line
# ll list whole current function
# w where (show call stack)
# u up (move up the stack)
# d down (move down the stack)
# b N set breakpoint at line N
# b file:N breakpoint at file:N
# h help
# h cmd help on a specific command
# q quit
post-mortem — drop into the crash site·python
# Run a script that crashes under pdb:
# python -m pdb script.py
# At the prompt, c to start running, you land at the crash
# Inside an interactive session:
import pdb
def bad():
items = [1, 2, 3]
return items[10]
try:
bad()
except IndexError:
pdb.post_mortem()
# (Pdb) p items
# [1, 2, 3]
# (Pdb) p len(items)
# 3
# Useful in REPL when you want to inspect what the function saw
Conditional breakpoints — without modifying code·python
# At the pdb prompt:
#
# b file.py:42, x > 100
# Sets a breakpoint at line 42 of file.py that fires only when x > 100
#
# Useful for: 'this loop iterates 1 million times,
# stop only when something specific happens'
# Or in code, conditional breakpoint() calls
def process(items):
for i, x in enumerate(items):
if x < 0:
breakpoint() # only stops on negative
# process x
Write a function find_first_negative(nums) that returns the index of the first negative number, or -1 if none. Deliberately put breakpoint() inside the loop. Run a small script that calls it with [1, 2, -5, 3]. At the pdb prompt, use p i, p x, n to step through, and c to continue. Then disable breakpoints with PYTHONBREAKPOINT=0 env var and confirm the script runs through cleanly.
Progress
Progress is local-only — sign in to sync across devices.