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(); theb,tbreak, andconditioncommands work in everypdbversion. - 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.
# 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])
$ 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:
(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:
(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:
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.
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.
Edge cases and failure modes
- A condition that raises (e.g.
qty < 0whenqtyis sometimesNone) makes pdb treat the breakpoint as hit and stops anyway — guard withisinstancechecks 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
NameErroron every hit. - Side-effecting conditions run on every hit — never put a mutation or a
printin a condition; usecommandsfor that instead. tbreakwith 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()orignorecount 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:
(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:
# .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.
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.
Related
- Post-mortem debugging with pdb.pm() — inspect the failing frame after the exception, the complement to stopping before it.
- Interactive debugging with pdb and ipdb — the full command vocabulary the conditional prompt inherits.
- CPU profiling with cProfile and py-spy — measure the per-hit overhead a hot condition adds instead of guessing.
- Memory profiling with tracemalloc — locate the iteration where retained bytes start to grow when the invariant is memory, not a value.
- Debugging async code and event loops — set predicates on a coroutine that only misbehaves on one scheduled turn.
← Back to Interactive Debugging with pdb and ipdb