Debugging with pdb
Go beyond breakpoint() with conditional breakpoints, post-mortem debugging of crashes, scripted sessions, and attaching pdb to a running process.
What you will learn
- Set, list and remove breakpoints, including conditional ones
- Move through the call stack with where, up and down, and watch values with display
- Debug a crash after the fact with post-mortem mode
- Control breakpoint() with PYTHONBREAKPOINT, and attach pdb to a running process
On this page
The debugging habits lesson taught a routine and the first pdb commands: breakpoint(), then p, n, s, c, l, w and q. That is enough when you know which line to stop on. This lesson covers what you need when you do not: stopping only in the interesting iteration, inspecting a crash after it happened, and looking inside a program that is already running.
To keep the sessions below reproducible, many are driven by a script that feeds pdb its commands as text. At your keyboard you type the same commands one at a time after the (Pdb) prompt.
Breakpoints you set from inside pdb
You do not have to edit the file to stop somewhere. At the prompt, break (or b) sets a breakpoint at a line number or function name, and continue runs until the next one. Here is the tax function from the beginner lesson, driven by a script:
"""Drive pdb from a list of commands, so the session can be checked."""
import ioimport osimport pdb
def total_with_tax(prices, rate): total = 0 for price in prices: total += price return total * 1 + rateThe driver feeds pdb its commands and prints the captured session:
commands = """\break 12continuep totalp ratep total * 1 + ratep total * (1 + rate)continue"""
out = io.StringIO()debugger = pdb.Pdb(stdin=io.StringIO(commands), stdout=out)result = debugger.runcall(total_with_tax, [10, 20], 0.1)
text = out.getvalue().replace(os.getcwd() + os.sep, "")print(text)print("returned", result)> c39_scripted_pdb.py(9)total_with_tax()-> total = 0(Pdb) Breakpoint 1 at c39_scripted_pdb.py:12(Pdb) > c39_scripted_pdb.py(12)total_with_tax()-> return total * 1 + rate(Pdb) 30(Pdb) 0.1(Pdb) 30.1(Pdb) 33.0(Pdb)returned 30.1runcall starts the function under the debugger, which stops on its first line. break 12 sets a breakpoint on the return, and continue runs the loop at full speed and stops there. Then four p commands inspect the values, including the two competing expressions: total * 1 + rate gives the wrong 30.1 and total * (1 + rate) gives 33.0. The last continue lets the function finish. Your typed commands would appear after each (Pdb) prompt, which a script cannot echo.
Useful companions of break:
| Command | Effect |
|---|---|
break |
List all breakpoints, with hit counts |
break 20 / break module.py:20 / break function |
Set a breakpoint at a line or function |
break 20, i == 3 |
Set a conditional breakpoint: stop only when the expression is true |
tbreak 20 |
A temporary breakpoint, removed after the first hit |
disable 1 / enable 1 / clear 1 |
Turn breakpoint 1 off, back on, or delete it |
until / unt 25 |
Run until the line number is greater than the current one, or until line 25: leaves a loop |
return |
Run until the current function is about to return |
Stop only when it matters: conditional breakpoints
A loop with ten thousand iterations is hopeless with n and c alone. Give the breakpoint a condition and pdb stops only when it holds. display prints an expression every time execution stops in that frame, and notes when its value has changed.
import ioimport osimport pdb
def running_total(numbers): total = 0 for i, n in enumerate(numbers): total += n return totalcommands = """\break 9, i == 3continuep i, n, totaldisplay totalnextnextcontinue"""
out = io.StringIO()debugger = pdb.Pdb(stdin=io.StringIO(commands), stdout=out)result = debugger.runcall(running_total, [5, 10, 15, 20, 25])
print(out.getvalue().replace(os.getcwd() + os.sep, ""))print("returned", result)> c39_ex_cond.py(7)running_total()-> total = 0(Pdb) Breakpoint 1 at c39_ex_cond.py:9(Pdb) > c39_ex_cond.py(9)running_total()-> total += n(Pdb) (3, 20, 30)(Pdb) display total: 30(Pdb) > c39_ex_cond.py(8)running_total()-> for i, n in enumerate(numbers):display total: 50 [old: 30](Pdb) > c39_ex_cond.py(9)running_total()-> total += n(Pdb)returned 75The breakpoint on line 9 has the condition i == 3, so pdb skipped the first three iterations. At the stop, p i, n, total printed (3, 20, 30). After display total, the next next moved to the for line and pdb reported that total changed from 30 to 50, because the line had run. The final continue finished the function with 75.
Post-mortem: inspect a crash after it happened
When a program has already crashed, you do not need to guess a breakpoint and reproduce it. Post-mortem debugging opens the debugger at the point where the exception was raised, with every frame’s variables still alive. Run the script under pdb:
$ python -m pdb crash.pyIf the script ends with an uncaught exception, pdb prints the traceback and drops into a (Pdb) prompt inside the failing frame. In an interactive session, import pdb; pdb.pm() does the same for the last exception, and pdb.post_mortem() does it from an except block. This script post-mortems a caught KeyError:
"""Inspect a crash after the fact, with a scripted post-mortem session."""
import ioimport osimport pdbimport sys
def average_by_group(rows): sums = {} for group, value in rows: sums[group] = sums[group] + value return sums
rows = [("a", 1), ("b", 2)]try: average_by_group(rows)except KeyError: traceback = sys.exc_info()[2]commands = """\wherep groupp valuep sumsupp rowsquit"""
out = io.StringIO()debugger = pdb.Pdb(stdin=io.StringIO(commands), stdout=out)debugger.reset()debugger.interaction(None, traceback)
print(out.getvalue().replace(os.getcwd() + os.sep, ""))> c39_postmortem.py(12)average_by_group()-> sums[group] = sums[group] + value(Pdb) c39_postmortem.py(18)<module>()-> average_by_group(rows)> c39_postmortem.py(12)average_by_group()-> sums[group] = sums[group] + value(Pdb) 'a'(Pdb) 1(Pdb) {}(Pdb) > c39_postmortem.py(18)<module>()-> average_by_group(rows)(Pdb) [('a', 1), ('b', 2)](Pdb)interaction is the lower-level call that post_mortem wraps. The session lands on line 12, and the bug is plain once you can look: group is 'a' and sums is empty, so sums[group] raises the KeyError. where (also w) lists the stack with an arrow at the current frame, up moves to the caller’s frame so you can look at its variables (rows), and down moves back.
Turning breakpoint() on and off
breakpoint() calls sys.breakpointhook(), which by default starts pdb. The environment variable PYTHONBREAKPOINT changes what it does, without touching the source. Setting it to 0 disables every breakpoint() call. Setting it to the dotted name of a callable runs that instead:
"""breakpoint() calls sys.breakpointhook(), which PYTHONBREAKPOINT controls."""
import osimport subprocessimport sys
child_code = """x = 41breakpoint()print("finished without stopping:", x + 1)"""
def run_child(setting): env = dict(os.environ, PYTHONBREAKPOINT=setting) command = [sys.executable, "-B", "-c", child_code] run = subprocess.run( command, env=env, capture_output=True, text=True, stdin=subprocess.DEVNULL, ) return run.stdoutprint(run_child("0"), end="")print(repr(run_child("builtins.print")))finished without stopping: 42'\nfinished without stopping: 42\n'With PYTHONBREAKPOINT=0 the program ran straight through. With builtins.print, the breakpoint() call became print(), which printed an empty line first. In real use, the value is often the entry function of a different debugger. breakpoint() has been available since 3.7. Run a test suite or a deployed service with PYTHONBREAKPOINT=0 and a forgotten breakpoint() cannot freeze it.
Attaching to a running process
Sometimes the problem is in a program that is already running: a server that has hung, a job that has been going for an hour. pdb can attach to a running Python process by its process ID Requires version 3.14 or later (PEP 768):
$ python -m pdb -p 1234The -p (or --pid) option takes the PID of a process running Python 3.14 or newer, as described in the pdb documentation. Here is a long-running script to try it on. Start it in one terminal and note the PID that it prints:
import osimport time
def work(n): total = 0 for i in range(n): total += i time.sleep(0.2) return total
print("pid", os.getpid(), flush=True)print(work(200))In a second terminal, attach with the PID. The commands you type are the usual ones. This session was recorded against the script above with the PID it printed:
$ python -m pdb -p 18660> /home/you/work.py(9)work()-> time.sleep(0.2)(Pdb) where /home/you/work.py(14)<module>()-> print(work(200))> /home/you/work.py(9)work()-> time.sleep(0.2)(Pdb) p i41(Pdb) p total861(Pdb) quitpdb stopped the program at whatever line it was executing, showed its stack, and let us read i and total from the live process. A process blocked in a system call, such as time.sleep, only responds once its next bytecode instruction runs, so expect a short wait. Attaching also needs operating-system permission to inspect the target, so it works on your own processes and may be refused for others’. When you only want a stack trace and do not need to step, faulthandler can dump every thread’s stack on a signal.
Common pitfalls
- Leaving
breakpoint()in code. A forgotten call freezes a program with nobody at the keyboard. Search for it before committing, or run withPYTHONBREAKPOINT=0. - A variable named like a pdb command.
n,c,landpare commands. Usep nor!nto see your variable. - Debugging in a test run with output capture. pytest captures output, which hides the pdb prompt. Use
pytest --pdbto drop into post-mortem on a failing test, orpytest -sto turn capture off. - Attaching to the wrong process. Check the PID. A shell command like
pgrep -f server.pyhelps, and the attach needs Python 3.14 or newer in the target, not only in your terminal.
Write down the pdb commands that stop running_total([5, 10, 15, 20, 25]) on the iteration where i == 3, print i, n and total as a tuple, and then run the function to the end. The breakpoint is on the line total += n. What does the tuple print?
Show solution
The commands are the ones from the session above:
break 9, i == 3continuep i, n, totalcontinueThe tuple is (3, 20, 30): at iteration 3 the value of n is 20, and total is still 5 + 10 + 15 = 30 because the stop happens before the += runs. The function returns 75.
This inventory report runs without any error, and all four numbers it prints are wrong. The data is apple,3,0.5, pear,12,0.25, fig,5,2.0, plum,20,0.1. The correct output is: total value 16.5, most valuable item fig (by quantity times price), low stock (5 or fewer) ['apple', 'fig'], and names apple, fig, pear, plum. Use breakpoint() or python -m pdb to inspect each function, find the bug in each, and fix it.
LINES = ["apple,3,0.5", "pear,12,0.25", "fig,5,2.0", "plum,20,0.1"]
def parse_line(line): name, qty, price = line.split(",") return name, int(qty), float(price)
def total_value(items): total = 0 for name, qty, price in items: total = qty * price return total
def most_valuable(items): best = None for item in items: if best is None or item[1] * item[2] < best[1] * best[2]: best = item return best[0]The report functions and the printing follow:
def low_stock(items, limit=5): return [name for name, qty, price in items if qty < limit]
def name_list(items): names = sorted(name for name, qty, price in items) return ", ".join(names[:-1])
items = [parse_line(line) for line in LINES]print("total value:", total_value(items))print("most valuable:", most_valuable(items))print("low stock:", low_stock(items))print("names:", name_list(items))total value: 2.0most valuable: applelow stock: ['apple']names: apple, fig, pearShow solution
Stopping inside each function and printing the values shows:
total_value: after the loop,p totalprints2.0, the value of the last item alone. The linetotal = qty * pricereplaces the total on every pass. It should betotal += qty * price.most_valuable: the comparison picks the item with the smallest value.p item[1] * item[2]andp best[1] * best[2]show the order is reversed. Use>.low_stock: the requirement says 5 or fewer, andfighas exactly 5. Useqty <= limit.name_list:p names[:-1]shows the slice drops the last name. Joinnames.
With those four edits, the script prints the correct report:
$ python c39_ex_stock_fixed.pytotal value: 16.5most valuable: figlow stock: ['apple', 'fig']names: apple, fig, pear, plumBoundary values like fig with exactly 5 are the ones a test should include, as in the test-driven workflow lesson.
Using the same running_total([5, 10, 15, 20, 25]) function, write the commands that stop only the first time n is greater than 12, print i, n and total as a tuple, and then finish the run. Use a temporary breakpoint. What does the tuple print?
Show solution
tbreak takes a condition like break does, and it deletes itself after the first hit:
tbreak 9, n > 12continuep i, n, totalcontinue$ python c39_ex_tbreak.py> c39_ex_tbreak.py(7)running_total()-> total = 0(Pdb) Breakpoint 1 at c39_ex_tbreak.py:9(Pdb) Deleted breakpoint 1 at c39_ex_tbreak.py:9> c39_ex_tbreak.py(9)running_total()-> total += n(Pdb) (2, 15, 15)(Pdb)returned 75The stop is at i == 2, where n is 15 and total is 15. Deleted breakpoint 1 shows the temporary breakpoint is gone, so the second continue runs to the end.
Summary
- Set breakpoints from the prompt with
break, make them conditional withbreak 9, i == 3, and usetbreak,until,returnanddisplayto avoid stepping line by line. - After a crash,
python -m pdb script.py,pdb.pm()orpdb.post_mortem()opens the debugger inside the failing frame. Usewhere,upanddownto look around. PYTHONBREAKPOINT=0disables everybreakpoint()call without editing code.python -m pdb -p PIDRequires version 3.14 or later attaches to a running Python 3.14+ process so you can inspect it without restarting.- The next lesson turns from “is it right?” to “is it fast?”.