Debugging with pdb¶
In this tutorial, you will learn to use pdb, the built-in Python debugger, to step
through code, inspect variables, and diagnose problems interactively.
Time commitment: 15–20 minutes
Prerequisites:
- Basic Python knowledge (functions, exceptions, control flow)
- Familiarity with running Python scripts from the command line
Learning objectives¶
By the end of this tutorial, you will be able to:
- Explain what
pdbis and when to use it - Use
breakpoint()to pause program execution - Navigate code with essential
pdbcommands (n,s,c,l,p,q) - Inspect variables and evaluate expressions in the debugger
Note: Because pdb is an interactive debugger that requires a terminal, the
debugging sessions in this tutorial are shown as example output rather than
executable code cells. To try the examples yourself, copy the code into a .py
file and run it from the command line.
What is pdb?¶
pdb is the built-in interactive source code debugger in Python. It allows you to:
- Pause your program at any point
- Step through code one line at a time
- Inspect the values of variables
- Evaluate arbitrary Python expressions
- Set breakpoints to pause at specific locations
Unlike print() statements or logging, pdb lets you explore the state of your
program interactively at the exact moment a problem occurs.
When to use pdb¶
Use the debugger when:
- You need to understand the flow of execution in unfamiliar code
- A bug is difficult to reproduce or understand from log output alone
- You want to inspect complex data structures at runtime
- You need to test different values interactively without restarting the program
For routine diagnostic output, logging (covered in the previous tutorials) is usually more appropriate.
The breakpoint() function¶
The easiest way to start the debugger is with the built-in breakpoint() function,
introduced in Python 3.7. When Python reaches a breakpoint() call, it pauses
execution and opens the pdb prompt.
Previously, the standard way was to write import pdb; pdb.set_trace(). The
breakpoint() function is simpler and has the same effect.
Example: Setting a breakpoint¶
Consider the following function that calculates a discount. Save this code in a
file called discount.py:
def calculate_discount(price: float, discount_percent: float) -> float:
"""Calculate the discounted price.
Args:
price: The original price.
discount_percent: The discount as a percentage (for example, 20 for 20%).
Returns:
The price after discount.
"""
breakpoint() # Execution will pause here
discount_amount = price * (discount_percent / 100)
final_price = price - discount_amount
return final_price
result = calculate_discount(100.0, 20.0)
print("Final price:", result)
When you run python discount.py, the program pauses at breakpoint() and
you see the pdb prompt:
> /path/to/discount.py(14)calculate_discount()
-> discount_amount = price * (discount_percent / 100)
(Pdb)
The -> arrow shows the next line that will execute. You are now in the debugger
and can type commands.
Essential pdb commands¶
Here are the commands you will use most often:
| Command | Shortcut | Description |
|---|---|---|
next |
n |
Execute the current line, then stop at the next line |
step |
s |
Step into a function call |
continue |
c |
Continue execution until the next breakpoint |
list |
l |
Show the source code around the current line |
print |
p |
Print the value of an expression |
quit |
q |
Quit the debugger and stop the program |
where |
w |
Show the call stack |
help |
h |
Show help for a command |
Walking through a debugging session¶
Using the discount.py example above, here is what a typical debugging session
looks like:
$ python discount.py
> /path/to/discount.py(14)calculate_discount()
-> discount_amount = price * (discount_percent / 100)
(Pdb) p price
100.0
(Pdb) p discount_percent
20.0
(Pdb) n
> /path/to/discount.py(15)calculate_discount()
-> final_price = price - discount_amount
(Pdb) p discount_amount
20.0
(Pdb) n
> /path/to/discount.py(16)calculate_discount()
-> return final_price
(Pdb) p final_price
80.0
(Pdb) c
Final price: 80.0
In this session:
p price-- Print the value ofprice(100.0)p discount_percent-- Print the value ofdiscount_percent(20.0)n-- Execute the current line and move to the nextp discount_amount-- Verify the calculated discountn-- Execute the next linep final_price-- Verify the final resultc-- Continue running the program to completion
next versus step¶
The difference between n (next) and s (step) is important:
n(next) executes the current line completely. If the line contains a function call, the entire function runs and you stop at the next line in the current function.s(step) steps into a function call. If the current line calls a function, the debugger enters that function and stops at its first line.
Use n when you trust a function works correctly and want to skip over it.
Use s when you want to investigate what happens inside a function.
Inspecting variables¶
At the (Pdb) prompt, you can do more than just print variables:
p expression-- Print the result of any Python expressionpp expression-- Pretty-print (useful for large data structures)- Type any Python expression -- You can evaluate any valid Python at the prompt
(Pdb) p price * 2
200.0
(Pdb) p type(price)
<class 'float'>
(Pdb) p [x for x in range(5)]
[0, 1, 2, 3, 4]
Viewing source code with list¶
The l (list) command shows the source code around the current line. The current
line is marked with ->. Use ll (long list) to see the entire current function.
(Pdb) l
10 discount_percent: The discount as a percentage.
11
12 Returns:
13 The price after discount.
14 """
15 -> discount_amount = price * (discount_percent / 100)
16 final_price = price - discount_amount
17 return final_price
Navigating the call stack¶
When debugging, you often need to understand how the program reached the current point. The call stack shows the chain of function calls.
w(where) -- Show the full call stacku(up) -- Move up one level in the call stack (to the calling function)d(down) -- Move down one level (back towards the current function)
Moving up and down the stack lets you inspect variables in different scopes without changing the actual execution point.
Setting breakpoints in pdb¶
Besides placing breakpoint() in your source code, you can set breakpoints
interactively from the (Pdb) prompt:
b lineno-- Set a breakpoint at a specific line numberb filename:lineno-- Set a breakpoint in a specific fileb function-- Set a breakpoint at the first line of a functionb lineno, condition-- Set a conditional breakpointcl(clear) -- Remove breakpoints
Conditional breakpoints are particularly useful. For example:
(Pdb) b 15, discount_percent > 50
Breakpoint 1 at /path/to/discount.py:15
This breakpoint only triggers when discount_percent exceeds 50.
Post-mortem debugging¶
Sometimes you want to investigate the state of your program after an exception has occurred. This is called post-mortem debugging.
There are two ways to do this:
python -m pdb script.py-- Run the script underpdb. If an unhandled exception occurs,pdbwill automatically start at the point of the exception.pdb.pm()-- Start post-mortem debugging in the interactive interpreter after an exception.
def find_average(numbers: list[int]) -> float:
"""Calculate the average of a list of numbers.
Args:
numbers: A list of integers.
Returns:
The arithmetic mean of the numbers.
"""
total = sum(numbers)
count = len(numbers)
return total / count
# This will work
print("Average of [1, 2, 3]:", find_average([1, 2, 3]))
# This will raise a ZeroDivisionError
try:
find_average([])
except ZeroDivisionError:
print("Caught ZeroDivisionError when averaging an empty list")
If you save the code above (without the try/except) in a file and run it with
python -m pdb script.py, the debugger will pause at the ZeroDivisionError:
$ python -m pdb script.py
> /path/to/script.py(1)<module>()
-> def find_average(numbers):
(Pdb) c
Average of [1, 2, 3]: 2.0
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
Uncaught exception. Entering post mortem debugging
> /path/to/script.py(8)find_average()
-> return total / count
(Pdb) p total
0
(Pdb) p count
0
(Pdb) p numbers
[]
Now you can see exactly why the error occurred: numbers was an empty list,
so count was 0, leading to division by zero.
Combining logging and debugging¶
Logging and debugging complement each other well:
- Use logging to identify where a problem occurs (look for unexpected log messages or missing expected messages)
- Use
pdbto investigate why the problem occurs (inspect variables, step through logic)
A common workflow:
- Notice unexpected behaviour
- Check log output to narrow down the problem area
- Add a
breakpoint()near the suspicious code - Run the program and inspect the state at the breakpoint
- Fix the bug
- Remove the
breakpoint()call
import logging
logger = logging.getLogger("order_processor")
def process_order(items: list[dict], tax_rate: float = 0.2) -> float:
"""Calculate the total cost of an order including tax.
Args:
items: A list of dictionaries with 'name', 'price', and 'quantity' keys.
tax_rate: The tax rate as a decimal (for example, 0.2 for 20%).
Returns:
The total cost including tax.
"""
logger.info("Processing order with %s items", len(items))
subtotal = 0.0
for item in items:
item_total = item["price"] * item["quantity"]
logger.debug("Item %s: %s x %s = %s", item["name"], item["price"], item["quantity"], item_total)
subtotal += item_total
tax = subtotal * tax_rate
total = subtotal + tax
logger.info("Order total: subtotal=%s, tax=%s, total=%s", subtotal, tax, total)
return total
# Example order
order = [
{"name": "Widget", "price": 9.99, "quantity": 3},
{"name": "Gadget", "price": 24.99, "quantity": 1},
]
total = process_order(order)
print("Order total: %.2f" % total)
Tips for effective debugging¶
Start from the error, work backwards. Read the traceback from bottom to top. Set your breakpoint just before the line that fails.
Form a hypothesis. Before starting the debugger, think about what you expect each variable to contain. Then check your assumptions.
Use logging to narrow scope. If the bug is in a large codebase, use log messages to identify the general area before reaching for
pdb.Remember to remove breakpoints. After fixing the bug, always remove any
breakpoint()calls from your code.Use
PYTHONBREAKPOINT=0to disable allbreakpoint()calls without removing them from the code. This is useful in production:PYTHONBREAKPOINT=0 python my_script.py
Exercises¶
These exercises are designed to be completed outside of Jupyter, using a text editor and the command line.
Exercise 1: Find the bug¶
The following function has a bug. Save it to a file, add a breakpoint() call,
and use pdb to find and fix the problem.
def celsius_to_fahrenheit(celsius: float) -> float:
"""Convert a temperature from Celsius to Fahrenheit."""
return celsius * 9 / 5 + 32
def fahrenheit_to_celsius(fahrenheit: float) -> float:
"""Convert a temperature from Fahrenheit to Celsius."""
return fahrenheit - 32 * 5 / 9 # Bug: operator precedence
# This should give us back the original value
original = 100.0
converted = celsius_to_fahrenheit(original)
back = fahrenheit_to_celsius(converted)
print(f"Original: {original}, Converted: {converted}, Back: {back}")
# Expected: Original: 100.0, Converted: 212.0, Back: 100.0
Hint: Place a breakpoint inside fahrenheit_to_celsius and inspect the
intermediate values.
Solution: The bug is in fahrenheit_to_celsius. The expression
fahrenheit - 32 * 5 / 9 is evaluated as fahrenheit - ((32 * 5) / 9) due
to operator precedence. The correct version is (fahrenheit - 32) * 5 / 9.
Exercise 2: Conditional breakpoints¶
Save the following code to a file and run it under pdb
(python -m pdb script.py). Set a conditional breakpoint that only triggers
when name is "error_item".
items = ["apple", "banana", "error_item", "cherry"]
for item in items:
name = item
processed = name.upper()
print(processed)
Hint: Use b lineno, name == "error_item" at the (Pdb) prompt.
Exercise 3: Post-mortem debugging¶
Save the following code to a file and run it with python -m pdb script.py.
When the exception occurs, inspect the variables to understand why it failed.
def lookup_user(users: dict, user_id: int) -> str:
"""Look up a user name by their ID."""
return users[user_id]
user_database = {1: "Alice", 2: "Bob", 3: "Charlie"}
print(lookup_user(user_database, 4)) # KeyError
Use p users and p user_id at the (Pdb) prompt to see why the lookup failed.
Summary¶
In this tutorial, you learned the following:
pdbis the built-in interactive debugger in Python, useful for inspecting program state and stepping through codebreakpoint()is the modern way to set a breakpoint in your code- The essential commands are
n(next),s(step),c(continue),l(list),p(print), andq(quit) w(where) shows the call stack, andu/dmove up and down the stack- Conditional breakpoints let you pause only when a specific condition is met
- Post-mortem debugging with
python -m pdblets you inspect the state after an unhandled exception - Logging and debugging complement each other: use logging to find the general
area of a problem, then use
pdbto investigate the details
Congratulations on completing all four tutorials! You now have a solid foundation in both logging and debugging with Python. For more advanced topics, explore the Recipes and Reference sections.