Print statements work for simple bugs. When a value is wrong, you add a print, run the code, and read the output. But when the bug is in a loop, a nested function call, or a conditional that rarely triggers, print statements become slow and frustrating. You add five prints, run the code, realize you need three more, and repeat.
Python's built-in debugger, pdb, lets you pause execution at any line, step through code one statement at a time, inspect every variable, and change values while the program is running. You can answer questions like "what is the value of x at this exact moment" and "did this if branch actually execute" without adding or removing any code beyond a single breakpoint. It is especially useful once a test from testing exceptions and edge cases starts failing and you need to see why.
Dropping into the debugger
The fastest way to use pdb is to insert a breakpoint directly in your code. Add one line at the spot you want to inspect, then run your program normally:
def calculate_total(prices, discount):
subtotal = sum(prices)
breakpoint()
if discount > 0:
subtotal *= (1 - discount)
return subtotalWhen Python reaches the breakpoint, execution pauses and control switches from your program to the debugger. The pdb prompt shows the current file and line:
> my_script.py(4)calculate_total()
-> if discount > 0:
(Pdb)From here you can inspect variables, step through code, or continue running.
The older syntax import pdb; pdb.set_trace() works identically. Use breakpoint() in new code because you can disable all breakpoints by setting the environment variable PYTHONBREAKPOINT=0 without editing any files.
Essential pdb commands
Once you are at the pdb prompt, a small set of commands covers most debugging sessions. Each command has a short one-letter or two-letter abbreviation.
Type n (next) to execute the current line and stop at the next line in the same function. This is your primary movement command when you want to see what happens line by line:
(Pdb) n
> my_script.py(5)calculate_total()
-> subtotal *= (1 - discount)Type s (step) to step into a function call. If the next line calls another function, step takes you inside that function so you can debug it too. Next would run the entire function and stop afterward.
Type c (continue) to run until the next breakpoint or until the program ends. Use this when you have seen enough of the current section and want to jump ahead.
Type p followed by a variable name to print its current value:
(Pdb) p subtotal
150.0
(Pdb) p discount
0.15Type pp instead of p for pretty-printed output of complex objects like dictionaries and nested lists. Type l (list) to see the surrounding source code with an arrow pointing to the current line.
Type q (quit) to exit the debugger and abort the program. Use this when you have identified the bug and are ready to fix the source code.
Setting breakpoints from inside pdb
You do not need to edit your source file to add breakpoints. From the pdb prompt, type b followed by a line number to set a breakpoint at that line in the current file:
(Pdb) b 12
Breakpoint 1 at /path/to/my_script.py:12Type b with a function name to break at the start of that function. Type b with no arguments to list all current breakpoints. Type cl (clear) followed by a breakpoint number to remove it.
Conditional breakpoints stop only when a condition is true. This is useful when a bug only appears inside a loop with specific data:
(Pdb) b 15, item == "target_value"
Breakpoint 2 at /path/to/my_script.py:15The breakpoint only triggers when the variable item equals "target_value". Without this condition, you would have to manually step through every iteration until the right one appears.
Post-mortem debugging
When your program crashes with an unhandled exception, pdb can take you to the exact line where the exception occurred. Run your script with the -m pdb flag and pdb stops at the failing line, letting you inspect the state at the moment of failure:
python -m pdb my_script.pyYou can print variables, examine the call stack, and understand why the crash happened without adding any breakpoints beforehand.
For exceptions that occur in a running Python session, use pdb.pm() to enter post-mortem debugging of the most recent exception. This takes you to the exact line where the crash occurred:
>>> 1 / 0
ZeroDivisionError: division by zero
>>> import pdb; pdb.pm()
> <stdin>(1)<module>()
(Pdb) Navigating the call stack
When your code has multiple layers of function calls, use w (where) to see the full call stack. The arrow points to the current frame:
(Pdb) w
/path/to/main.py(10)<module>()
-> result = process(data)
/path/to/utils.py(5)process()
-> return validate(items)
> /path/to/utils.py(12)validate()
-> if not item:Type u (up) to move to the calling function and inspect its variables. Type d (down) to move back. This lets you trace how data flows through multiple function calls without restarting the debugger.
A complete debugging session
Imagine a function that calculates shipping cost but returns zero for certain weights. You suspect the discount logic is wrong but are not sure where:
def shipping_cost(weight, is_member):
base = weight * 2.5
if is_member:
base = base * 0.8
if weight > 10:
base = 0
return baseInsert breakpoint() on the line just before the weight check, run the program with real input, and step through the logic to watch how base changes:
> shipping.py(6)shipping_cost()
-> if weight > 10:
(Pdb) p weight
12
(Pdb) p is_member
True
(Pdb) p base
24.0
(Pdb) n
> shipping.py(7)shipping_cost()
-> base = 0
(Pdb) In three commands you identified the bug: the weight threshold resets the cost to zero instead of applying a different rate. You found what would have taken several print-debug cycles in under a minute with pdb.
The next article covers debugging Python programs in VS Code, which gives you the same debugging power with a visual interface, breakpoints you set by clicking, and variable values displayed in a panel.
Rune AI
Key Insights
- Insert breakpoint() or import pdb; pdb.set_trace() at any line to drop into the debugger when that line executes.
- n (next) steps over function calls; s (step) steps into them; c (continue) runs until the next breakpoint.
- p variable prints the value of a variable; pp variable pretty-prints complex objects; l lists surrounding source code.
- b lineno sets a breakpoint at a specific line; b function_name sets one at the start of a function.
- q quits the debugger and aborts the program; use it when you have found the bug and are ready to fix the code.
Frequently Asked Questions
What is the difference between pdb.set_trace() and breakpoint()?
How do I debug a script that reads from standard input?
Can I debug a Python program that is already running?
Conclusion
pdb turns debugging from a guessing game into a step-by-step investigation. Set a breakpoint, run your code, and walk through it one line at a time while watching the variables change. The few minutes you spend learning pdb commands will save hours of adding and removing print statements.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.