Debugging & Performance

Setting Conditional Breakpoints in pdb

You are stepping through a loop that processes 50,000 records and the bug only shows on one of them. A plain breakpoint stops on every iteration, so you hammer continue hundreds of times before reaching the interesting one — or give up. A conditional breakpoint stops only when a predicate is true, dropping you into the prompt on the exact iteration where the invariant breaks.

Prerequisites

  • Python 3.7+ for breakpoint(); the b, tbreak, and condition commands work in every pdb version.
  • Familiarity with entering the debugger and the basic command loop from interactive debugging with pdb and ipdb.

Solution

There are two ways to make pdb stop conditionally: a line breakpoint with an inline condition, and a guarded breakpoint() call in source. Start with the command form, which requires no source edits.

Python
# orders.py
def process(orders):
    total = 0
    for i, qty in enumerate(orders):
        total += qty          # we suspect a negative qty corrupts the total
    return total

if __name__ == "__main__":
    process([5, 3, 8, -2, 9, 4])
Bash
$ python -m pdb orders.py
(Pdb) b orders.py:5, qty < 0     # break at line 5 ONLY when qty is negative
Breakpoint 1 at orders.py:5
(Pdb) c                          # run; pdb skips every iteration until the predicate holds
> orders.py(5)process()
-> total += qty
(Pdb) p i, qty                   # we land exactly on the bad iteration
(3, -2)

The condition is any expression valid in the target frame; pdb evaluates it on every hit and stops only when it is truthy. To attach a condition to an existing breakpoint, or change one, reference the breakpoint by its number:

Bash
(Pdb) b orders.py:5             # unconditional breakpoint, gets number 1
Breakpoint 1 at orders.py:5
(Pdb) condition 1 qty < 0       # retrofit the predicate onto breakpoint 1
(Pdb) condition 1               # omit the expression to clear it again

For a breakpoint you only ever want to hit once — the common case inside a hot loop — use tbreak. It fires a single time and removes itself, so there is no leftover breakpoint to disable afterward:

Bash
(Pdb) tbreak orders.py:5, qty < 0    # one-shot: auto-removed after the first hit
Breakpoint 2 at orders.py:5
(Pdb) c
> orders.py(5)process()
-> total += qty
(Pdb) c                              # continues to the end; breakpoint is already gone

A conditional breakpoint stops you before the invariant breaks so you can watch the corruption happen; when the exception has already fired and you only care about the state at the moment of failure, reach instead for post-mortem debugging with pdb.pm(), which reopens the raising frame without re-running the loop.

When you would rather express the predicate in source — for example because the condition is expensive or spans multiple statements — guard a breakpoint() call with an if:

Python
def process(orders):
    total = 0
    for i, qty in enumerate(orders):
        if qty < 0:               # only enter the debugger on the offending value
            breakpoint()
        total += qty
    return total

You can also raise the hit count with ignore: ignore 1 100 tells pdb to skip the next 100 hits of breakpoint 1 before honouring it — useful when you know the bug is "around iteration 100" but cannot express it as a value predicate.

Why this works

pdb stores conditions on the breakpoint object and evaluates them inside the paused frame's namespace via eval, so any in-scope name or expression is fair game. Because evaluation happens on every hit, a condition is functionally identical to a guarded breakpoint() — the command form just keeps the predicate out of your source. tbreak is a normal breakpoint with a temporary=True flag that pdb clears on first fire, and ignore decrements a counter before the condition is even checked.

The decision pdb runs every time a conditional breakpoint line is reached When the breakpoint line is reached, pdb first checks the ignore counter: if it is greater than zero it decrements it and keeps running without stopping. Otherwise it evaluates the stored condition inside the paused target frame. If the result is falsy pdb continues to the next hit; if it is truthy the (Pdb) prompt opens on that iteration. Both skip paths feed a single "loop keeps running" outcome. A separate note explains that when the breakpoint is a tbreak, its temporary flag is cleared the instant the prompt opens, so it removes itself and never fires again. What pdb decides each time the breakpoint line is reached prompt / stop keep running line reached on this pass ignore count > 0 ? yes → decrement no eval(condition) in the paused target frame result truthy ? false → skip true (Pdb) prompt opens you land on this iteration loop keeps running pdb runs on to the next time the line is hit — no prompt, no break in your flow. both skip paths land here after the prompt if the breakpoint is a tbreak temporary=True is cleared the instant the prompt opens — it removes itself and can never fire a second time
Every hit runs the same gate: the ignore counter is decremented first (and swallows the hit while it is positive), then the condition is eval'd in the paused frame. Only a truthy result opens the prompt; a tbreak additionally clears its temporary=True flag the moment it fires, so it fires exactly once.

That per-hit eval is exactly why a hot predicate has a measurable cost: the decision above runs on every pass through the line, so a condition on a million-iteration loop shows up in a CPU profile of the run as time spent in the debugger's trace callback rather than your code.

The three ways to skip uninteresting hits differ in cost and in what they can express.

Three ways to stop only where it matters A table comparing a breakpoint condition, an ignore count and a guarded breakpoint call, across what each expresses, its per-hit cost, and whether it needs a source edit. Three ways to stop only where it matters Criterion Expresses Cost per hit condition on a breakpoint any in-frame expression eval, every hit ignore N skip a fixed count a decrement guarded breakpoint() arbitrary code a normal if commands + continue log without stopping eval plus print
A guarded call is cheapest on a hot line; an ignore count is cheapest of all when you can express the target as an iteration number.

Edge cases and failure modes

  • A condition that raises (e.g. qty < 0 when qty is sometimes None) makes pdb treat the breakpoint as hit and stops anyway — guard with isinstance checks inside the expression if the variable's type varies.
  • Conditions evaluate in the target frame, not where you typed them; a name that exists in your prompt frame but not the breakpoint's frame raises NameError on every hit.
  • Side-effecting conditions run on every hit — never put a mutation or a print in a condition; use commands for that instead.
  • tbreak with a condition still consumes its single life on the first time the condition is true, not the first time the line is reached.
  • Conditional breakpoints add per-hit overhead; on a million-iteration loop the eval cost is noticeable, so prefer a guarded breakpoint() or ignore count when the predicate is hot.

Scripting a session with commands and .pdbrc

Typing the same three commands at every stop is what makes debugging feel slow. pdb can script both the per-breakpoint actions and the session defaults.

commands attaches a command list to a breakpoint. The list runs automatically each time that breakpoint fires, and ending it with continue turns the breakpoint into a logging probe that never stops execution:

Bash
(Pdb) b orders.py:5, qty < 0
Breakpoint 1 at orders.py:5
(Pdb) commands 1
(com) p i, qty, total          # print the state at every negative quantity
(com) continue                 # ... and keep running: a probe, not a stop
(com) end
(Pdb) c

That pattern is the fastest way to answer "how often, and with what values" without adding a print statement to source you may not own — and because it lives in the debugger, it disappears when the session ends.

.pdbrc holds commands that run at the start of every session. A project-level file (in the working directory) plus a personal one (in $HOME) covers both shared and individual defaults:

Plain text
# .pdbrc — runs on every pdb session in this project
alias loc pp {k: v for k, v in locals().items() if not k.startswith('_')}
alias sql pp [str(q) for q in connection.queries[-5:]]
display self.state

alias defines a shorthand, display pins an expression that re-prints after each step, and both survive the whole session. Note that .pdbrc is executed as pdb commands, not Python, and that pdb reads the home file first and the local one second, so a project alias can override a personal one.

Two cautions. A .pdbrc that references names not present in every session prints an error at each start — keep aliases lazy by putting the risky expression inside the alias body rather than in a display line. And never commit a .pdbrc containing continue or quit: a stray control command turns every colleague's debugging session into an immediate exit, and the cause is not obvious.

A scripted breakpoint that never stops A vertical flow of a logging probe: set a conditional breakpoint, attach a command list that prints the interesting state, end the list with continue so execution resumes, and read the accumulated output instead of stepping. A scripted breakpoint that never stops set the condition b file:line, expr The predicate still runs every hit attach commands p the state you need pp formats structures readably end with continue the probe never stops Removes the need to step read the log frequency and values Delete the breakpoint to stop logging
A breakpoint that prints and continues gives you tracing without editing the source under test.

Frequently Asked Questions

How do I make pdb break only when a variable has a specific value? Set the breakpoint with a condition: b orders.py:42, qty < 0. pdb evaluates the expression in the target frame on every hit and only stops when it is truthy, so the prompt opens on the iteration where qty goes negative.

What is the difference between break and tbreak in pdb?break (b) sets a persistent breakpoint that stops every time the line is reached. tbreak sets a temporary breakpoint that is removed automatically the first time it fires, which is ideal for stopping once inside a hot loop.

Can I add a condition to a breakpoint I already set? Yes. Use condition bpnumber expression to attach or change the condition on an existing breakpoint by its number, or condition bpnumber with no expression to clear it and make the breakpoint unconditional again. Can I set a breakpoint that fires only in one worker or one process? Yes — make the process identity part of the predicate. A guarded breakpoint() wrapped in if os.environ.get("PYTEST_XDIST_WORKER") == "gw3": opens the prompt in one worker only, which is the practical way to debug a failure that occurs on a single parallel worker without serialising the whole suite.

← Back to Interactive Debugging with pdb and ipdb