Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

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

Sponsored

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 pdb to 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. pdb is 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 / b in pdb executes 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-mortem when 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. Use breakpoint() which is a no-op in production unless PYTHONBREAKPOINT is 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 pdb as 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() inside pdb instead of qexit() doesn't work; use q or Ctrl+D to quit the debugger.
  • Forgetting to remove breakpoint() from production code — it's a no-op unless PYTHONBREAKPOINT is set, but it's better practice to only add it during development.
  • Trying to use pdb on a script that's inside a virtual environment without activating it — the pdb module itself is part of the standard library, but the script might rely on installed packages. Always activate your venv first.
  • Assuming post-mortem works for exceptions caught in a try/except — it only triggers for unhandled exceptions unless you manually call pdb.post_mortem() inside the except clause.

Variations

  1. Use pdb.run() to execute arbitrary Python strings inside the debugger for quick testing.
  2. Integrate pdb with unittest test cases by calling pdb.set_trace() (or breakpoint()) inside a failing test to inspect the exact state.
  3. Try ipdb (IPython debugger) for syntax-highlighted, tab-completion enhanced debugging with pip install ipdb.

Real-world use cases

  • Debugging a complex data pipeline where a KeyError appears only for specific input records — use python -m pdb pipeline.py and 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 trigger pdb.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 pdb commands like where to see the thread-local stack.

Key takeaways

  • Replace print() debugging with breakpoint() for interactive, surgical inspection of your code mid-execution.
  • Post-mortem debugging with python -m pdb script.py automatically 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), and q (quit) — these cover 95% of your debugging needs.
  • Post-mortem only works for unhandled exceptions; for caught exceptions, insert pdb.post_mortem() inside the except block.
  • Always remove breakpoint() calls from production code to avoid unintended pauses—use a linter rule or pre-commit hook to catch them.
  • Use pdb for complex, intermittent, or deep-stack bugs; use print() only for trivial, known issues.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.