Debugging Python with pdb
Every Python developer encounters bugs. Sometimes print() statements are enough to track down the problem. But when your code does something unexpected and you can’t figure out why, a debugger lets you inspect what’s actually happening as your program runs. Python ships with a built-in debugger called pdb.
In this tutorial, you’ll learn how to set breakpoints, step through code line by line, inspect variables, and use pdb to find bugs faster.
Setting a Breakpoint
The simplest way to enter the debugger is to add a breakpoint at the line where you want to pause execution. In Python 3.7+, you can use the built-in breakpoint() function:
def calculate_total(prices):
total = 0
for price in prices:
breakpoint() # Execution stops here
total = total + price
return total
cart = [10, 20, 5.50]
print(calculate_total(cart))
When you run this script, execution pauses at the breakpoint() line and displays the pdb prompt:
> example.py(5)calculate_total()
-> total = total + price
(Pdb)
You can also use the older method, which works in all Python versions:
import pdb
pdb.set_trace()
Both do the same thing—pause execution and give you control.
Essential pdb Commands
Once you’re at the (Pdb) prompt, several commands help you navigate and inspect your code.
Stepping Through Code
The step (or s) command executes the current line and stops at the next one. If the current line calls a function, step takes you inside that function:
(Pdb) s
The next (or n) command is similar, but it runs the entire function call without stopping inside it:
(Pdb) n
The continue (or c) command runs the program until it hits another breakpoint or finishes:
(Pdb) c
Inspecting Variables
The p command prints the value of an expression:
(Pdb) p total
15
(Pdb) p price
5.5
Use pp for pretty-printed output, which is useful for complex data structures:
(Pdb) pp prices
[10, 20, 5.5]
The args (or a) command displays all arguments passed to the current function:
(Pdb) a
prices = [10, 20, 5.5]
Viewing Code
The list (or l) command shows the source code around your current position:
(Pdb) l
3 def calculate_total(prices):
4 total = 0
5 -> for price in prices:
6 breakpoint()
7 total = total + price
8 return total
The longlist (or ll) command shows the entire current function.
Navigating the Call Stack
When you’re deep inside nested function calls, these commands help you move around:
where(orw) — shows the full call stackup(oru) — moves to the caller framedown(ord) — moves back to the called frame
(Pdb) w
example.py(10)<module>()
-> calculate_total(cart)
> example.py(5)calculate_total()
-> for price in prices:
(Pdb) u
> example.py(10)<module>()
-> calculate_total(cart)
Setting Breakpoints Without Code Changes
You don’t always need to modify your source code to debug it. You can launch pdb from the command line:
python -m pdb your_script.py
The debugger stops at the first line of your program, then you can set breakpoints using the break command:
(Pdb) break 6
Breakpoint 1 at example.py:6
(Pdb) continue
You can also set breakpoints at specific line numbers or function names:
(Pdb) break calculate_total
Breakpoint 1 at example.py:3
Conditional breakpoints stop only when a condition is true:
(Pdb) break 7, price > 15
Breakpoint 1 at example.py:7
This is useful when you only care about a specific case in a loop.
Post-Mortem Debugging
When your program crashes, you can immediately inspect the traceback using post-mortem debugging. Run your script normally, then after it crashes, start pdb:
def divide(a, b):
return a / b
divide(10, 0)
After the ZeroDivisionError, run:
python -m pdb -c "post-mortem" your_script.py
Or from within Python:
import pdb
import sys
def divide(a, b):
return a / b
try:
divide(10, 0)
except:
pdb.pm()
The pm() function enters post-mortem debugging using sys.last_exc_info.
Debugging a Real Problem
Here’s a practical example. Suppose this function has a bug:
def find_average(numbers):
total = sum(numbers)
count = len(numbers)
return total / count
scores = [85, 90, None, 78]
print(find_average(scores))
Running this produces a TypeError because None can’t be added to integers. Let’s debug it:
def find_average(numbers):
total = sum(numbers)
count = len(numbers)
breakpoint() # Add this line
return total / count
scores = [85, 90, None, 78]
print(find_average(scores))
When pdb stops at the breakpoint, inspect what’s happening:
(Pdb) p total
253
(Pdb) p count
4
(Pdb) p numbers
[85, 90, None, 78]
Now you can see the problem—None is in the list. You can fix this by filtering out None values:
def find_average(numbers):
valid_numbers = [n for n in numbers if n is not None]
total = sum(valid_numbers)
count = len(valid_numbers)
return total / count if count > 0 else 0
scores = [85, 90, None, 78]
print(find_average(scores))
This prints 84.33333333333333.
Next Steps
Now you know how to debug Python programs using pdb. The skills in this tutorial transfer to other debuggers too—IDE debuggers like PyCharm or VS Code work similarly, just with a graphical interface.
The next tutorial in this series covers writing automated tests with pytest. Tests help you catch bugs before they reach production, and they’re especially useful when combined with debugging skills.
Summary
- Use
breakpoint()orpdb.set_trace()to pause execution - Step through code with
step,next, andcontinue - Inspect variables with
p,pp, andargs - Navigate the call stack with
where,up, anddown - Set breakpoints from the command line or conditionally
- Use post-mortem debugging to investigate crashes