Debug with pdb and post-mortem
Master debugging with pdb and post-mortem inspection in Python. This lesson covers core concepts, step-by-step walkthroughs, troubleshooting, and practical exercises for effective debugging.
Focus: debug with pdb and post-mortem inspection
You're staring at a cryptic IndexError or a KeyError that only appears in production, and your code is littered with print() statements that you'll have to remove later. This is the print-debugging prison, and it's time to break free. Python's built-in pdb (Python Debugger) gives you superpowers: the ability to pause execution, inspect variables, step through code line by line, and even debug a program after it has crashed using post-mortem inspection. This lesson will replace your print() hacks with professional, surgical debugging tools.
The problem this lesson solves
Debugging is an inevitable part of development, but most beginners default to a scattergun approach: adding print() calls everywhere, rerunning the program, and trying to mentally reconstruct the program's state. This is slow, error-prone, and pollutes your code. When you encounter a bug that only occurs after a hundred iterations or a specific user input, print statements become completely impractical.
What happens without pdb?
- You waste time inserting and removing print statements.
- You can only see values at the points where you inserted prints, not the dynamic flow.
- You cannot pause execution to inspect complex data structures mid-run.
- You have no way to examine the state of a program after it has crashed.
This lesson solves all of that by teaching you how to use pdb to set breakpoints, step through code, and perform post-mortem analysis. By the end, you'll be able to diagnose and fix bugs faster and with more confidence.
Core concept / mental model
Think of pdb as a time-travel microscope for your code. You can pause at any line (set a breakpoint), rewind to see what happened (step back is limited, but you can restart), and zoom into any variable's value.
Key definitions
- Breakpoint: A deliberate pause point in your code where execution stops, and you can inspect the environment.
- Stepping: Moving through code line by line (
next,step). - Post-mortem debugging: Using
pdbto automatically launch a debugger at the exact location where an unhandled exception occurred, so you can inspect the call stack and variables at the moment of failure.
Mental model: Imagine you're a detective walking through a crime scene (your running program). You can't just look at photos (prints). You need to be in the room, pick up objects (inspect variables), and ask witnesses (the call stack) what happened.
pdbis your detective badge.
How it works step by step
1. Starting pdb
You can start pdb in two ways:
- Script mode: Run your script directly with
python -m pdb my_script.py. This starts the debugger before any code runs. - Inline mode: Insert a breakpoint in your code using
breakpoint()(Python 3.7+). This pauses execution exactly at that line when the script runs normally.
2. Basic pdb Commands
Once inside pdb, you use these essential commands:
| Command | Shortcut | Action |
|---|---|---|
l |
l |
List source code around the current line |
n |
n |
Execute the current line and stop at the next line in the same function |
s |
s |
Step into a function call (step inside) |
c |
c |
Continue execution until the next breakpoint |
q |
q |
Quit the debugger |
p <expression> |
p |
Print the value of an expression (e.g., p my_var) |
pp <expression> |
pp |
Pretty-print a complex expression (e.g., pp my_dict) |
b <line_number> |
b |
Set a breakpoint at a specific line number |
where |
w |
Show the current call stack |
!<statement> |
- | Execute a Python statement in the current context (e.g., !my_var = 0) |
3. Post-Mortem Debugging
Post-mortem debugging is the most powerful technique for handling crashes. Instead of wrapping everything in try/except and printing, you can run your program, let it crash, and then inspect the crash site.
How to do it:
python -m pdb my_script.py
If my_script.py crashes, pdb will automatically enter post-mortem mode at the crash point. You can then use l, p, w to explore.
Alternatively, you can use pdb.pm() after an exception in a live session:
import pdb
def buggy_function():
return 1 / 0
try:
buggy_function()
except:
pdb.post_mortem()
Hands-on walkthrough
Example 1: Basic breakpoint and inspection
Create a file called example1.py:
def divide(a, b):
breakpoint() # Execution pauses here
return a / b
def main():
x = 10
y = 0
result = divide(x, y)
print(f"Result: {result}")
main()
Run it:
python example1.py
In the pdb prompt:
(Pdb) l
1 def divide(a, b):
2 breakpoint() # Execution pauses here
3 -> return a / b
4
5 def main():
6 x = 10
7 y = 0
8 result = divide(x, y)
9 print(f"Result: {result}")
10
11 main()
(Pdb) p a
10
(Pdb) p b
0
(Pdb) p a / b
ZeroDivisionError: division by zero
(Pdb) q
Pro tip: Using
p a / binpdbexecutes the expression, so you can test potential fixes before changing your code.
Example 2: Stepping through a loop
Create example2.py:
def process_items(items):
result = []
for i, item in enumerate(items):
breakpoint()
result.append(item.upper())
return result
items = ["apple", "banana", 123]
processed = process_items(items)
print(processed)
Run it. At the first breakpoint, loop through:
(Pdb) p i, item
(0, 'apple')
(Pdb) n
--Return--
> ...
(Pdb) n
(Pdb) c
At the second iteration (i=1), inspect again:
(Pdb) p i, item
(1, 'banana')
(Pdb) c
At the third iteration, the program will crash because 123 has no .upper(). But wait! You used c, so execution continues to the next breakpoint. You can use n to execute the .upper() line and see the error, or better yet, use s if you wanted to step into something.
Example 3: Post-mortem inspection
Create example3.py:
import pdb
def load_config(file_path):
with open(file_path, 'r') as f:
return f.read()
def parse_config(data):
config = {}
for line in data.split('\n'):
key, value = line.split('=')
config[key.strip()] = value.strip()
return config
def main():
# This file doesn't exist yet
raw = load_config("config.ini")
config = parse_config(raw)
print(config)
if __name__ == "__main__":
main()
Run with implicit post-mortem:
python -m pdb example3.py
The debugger starts but doesn't crash yet. Type c to continue:
(Pdb) c
Traceback (most recent call last):
File "example3.py", line 17, in main
raw = load_config("config.ini")
File "example3.py", line 4, in load_config
with open(file_path, 'r') as f:
FileNotFoundError: [Errno 2] No such file or directory: 'config.ini'
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
Now you're in post-mortem! You can inspect the crash site:
(Pdb) l
2 with open(file_path, 'r') as f:
3 return f.read()
4 ->
5 def parse_config(data):
6 config = {}
7 for line in data.split('\n'):
8 key, value = line.split('=')
9 config[key.strip()] = value.strip()
10 return config
11
(Pdb) p file_path
'config.ini'
(Pdb) w
...
-> main()
...
-> raw = load_config("config.ini")
...
-> with open(file_path, 'r') as f:
(Pdb) q
You can see exactly what file it tried to open. Now you know to create config.ini or fix the path.
Compare options / when to choose what
| Tool / Technique | Best for | Setup complexity | Learning curve |
|---|---|---|---|
print() |
Quick, obvious bugs | Very low | None |
breakpoint() / pdb |
Complex bugs, loops, deep stack traces | Low | Medium |
post-mortem (pdb.pm()) |
Crashes where you don't know the call stack | Low | Medium |
| IDE debugger (e.g., VS Code, PyCharm) | GUI lovers, long sessions | Medium | Low-High |
- Use
print()when you know exactly what you're looking for and the code is simple. - Use
breakpoint()when you need to pause mid-execution and explore interactively. - Use
post-mortemwhen a crash happens and you have no idea where to start. - Use an IDE debugger if you spend hours debugging complex applications and prefer a visual interface.
Pro tip: Never use
pdb.set_trace()in code you'll commit. Usebreakpoint()which is a no-op in production unlessPYTHONBREAKPOINTis set.
Troubleshooting & edge cases
Mistake 1: Forgetting to quit pdb and breaking the program flow
Symptom: You type q and the program just exists. Or you accidentally type c and it runs to completion.
Fix: Use q (quit) when you want to stop; use c (continue) to let it run to the next breakpoint or end.
Mistake 2: Trying to inspect local variables outside the current frame
Symptom: You see NameError when trying to p some_var.
Fix: Use w (where) to see the call stack, then use u (up) and d (down) to move between frames. For example, from divide() you can move up to main() to see the caller's variables.
Mistake 3: Post-mortem not activating for caught exceptions
Symptom: You wrapped code in try/except, but pdb doesn't stop at the crash.
Fix: post-mortem only works for uncaught exceptions. If you have a try/except, you need to call pdb.post_mortem() from inside the except block.
Example of manual post-mortem:
import pdb
def risky():
return 1 / 0
try:
risky()
except:
import traceback
traceback.print_exc()
pdb.post_mortem()
Now, even with the try/except, you get a full debugger session.
Mistake 4: Breakpoints not hitting due to module caching or .pyc files
Fix: Delete __pycache__/ or run with python -B to disable .pyc generation. Also ensure you're editing the correct file (not a stale version).
What you learned & what's next
You've mastered the core concepts of debugging with pdb and post-mortem inspection:
- Explain the core idea: You understand
pdbas an interactive interpreter that pauses execution, allowing inspection and manipulation of the program state. - Complete a practical exercise: You ran examples with breakpoints, stepping, and post-mortem, and you can now diagnose crashes by inspecting the call stack and local variables at the crash site.
What's next? In the next lesson, you'll learn about logging in Python — a more permanent, scalable way to record program behavior. Logging complements debugging: you use logging to understand what happened in production, and pdb to diagnose issues during development. You'll see how to use Python's logging module to add structured, leveled messages to your code.
Practice recap
Try this: Create a Python script that processes a CSV file with some malformed rows. The script should crash on row 47 with a ValueError. Run it with python -m pdb script.csv and use l, p, and w to inspect the malformed data at the crash site. Then modify the script to skip malformed rows and use pdb again to verify the fix. This will cement your post-mortem skills.
Common mistakes
- Typing
exit()insidepdbinstead ofq—exit()doesn't work; useqor Ctrl+D to quit the debugger. - Forgetting to remove
breakpoint()from production code — it's a no-op unlessPYTHONBREAKPOINTis set, but it's better practice to only add it during development. - Trying to use
pdbon a script that's inside a virtual environment without activating it — thepdbmodule itself is part of the standard library, but the script might rely on installed packages. Always activate your venv first. - Assuming
post-mortemworks for exceptions caught in atry/except— it only triggers for unhandled exceptions unless you manually callpdb.post_mortem()inside theexceptclause.
Variations
- Use
pdb.run()to execute arbitrary Python strings inside the debugger for quick testing. - Integrate
pdbwithunittesttest cases by callingpdb.set_trace()(orbreakpoint()) inside a failing test to inspect the exact state. - Try
ipdb(IPython debugger) for syntax-highlighted, tab-completion enhanced debugging withpip install ipdb.
Real-world use cases
- Debugging a complex data pipeline where a
KeyErrorappears only for specific input records — usepython -m pdb pipeline.pyand set conditional breakpoints to inspect each record. - Investigating a web API endpoint that returns a 500 error inconsistently — wrap the endpoint handler with
@breakpoint()or triggerpdb.pm()after the exception log. - Reproducing a bug in a multi-threaded application by setting a breakpoint in a thread function and stepping through each thread's execution with
pdbcommands likewhereto see the thread-local stack.
Key takeaways
- Replace
print()debugging withbreakpoint()for interactive, surgical inspection of your code mid-execution. - Post-mortem debugging with
python -m pdb script.pyautomatically pauses at the exact crash line, showing you the call stack and variable values at the moment of failure. - Master the core pdb commands:
n(next),s(step into),c(continue),p(print),l(list),w(where), andq(quit) — these cover 95% of your debugging needs. - Post-mortem only works for unhandled exceptions; for caught exceptions, insert
pdb.post_mortem()inside theexceptblock. - Always remove
breakpoint()calls from production code to avoid unintended pauses—use a linter rule or pre-commit hook to catch them. - Use
pdbfor complex, intermittent, or deep-stack bugs; useprint()only for trivial, known issues.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.