How do you use pdb's conditional breakpoints to catch a duplicated side effect in a 6,800-row batch?
answer
- Do not step through thousands of rows
- Stop on the effect, not the loop
- Attach a predicate to the breakpoint
- Compare the stacks of both firings
- up and down move inspection, not execution
basics
~10 sBreak on the code that performs the side effect, not on the loop, with a condition so it fires only for the offending row. Then use where and up to compare the caller frames.
solid answer
~50 sStepping 6,800 iterations by hand is not a strategy. Put a breakpoint where the effect actually happens — `break dispatch.py:120, row_id == 6789` — so the debugger stops only on the row that is duplicated. Alternatively set the breakpoint unconditionally, note its number from `break` with no arguments, then attach `condition <bpnum> <expr>` or `ignore <bpnum> <count>` to skip the first N hits. When it fires, `where` prints the stack, `up` and `down` move the inspection frame without executing anything, and `p`/`pp` evaluate expressions in whichever frame you are standing in — so you can read the caller's locals and see which of two code paths reached the effect. Let it fire a second time and compare the two stacks: the difference *is* the bug. If you cannot edit the source, `pdb.runcall(fn, *args)` starts the debugger at that call's entry.
code
python · 10 linesimport os
import pdb
def apply_fee(row):
if row["id"] == 6789 and os.environ.get("ROUTE_DEBUG"):
pdb.set_trace()
row["fee"] = round(row["fee"] * 0.9, 2)
return row
print(apply_fee({"id": 6789, "fee": 100.0}))go deeper
Know that a breakpoint can carry a condition so it fires only when an expression is true, and that where prints the call stack. Being able to say you would stop on the failing row rather than every row is already the right instinct.
Explain the mechanics: break file:line with a condition, break with no arguments to list numbers, condition and ignore to adjust an existing breakpoint, and up and down to inspect caller frames without resuming execution.
Show the diagnostic method: instrument where the effect happens, capture and compare both stacks, reproduce on a one-row batch, and know the limits — the pause is per-thread, conditions cost time on every pass, and concurrency-shaped duplicates need logging rather than a prompt.
Talk about avoiding the class of bug: idempotent effects keyed by row identity, so a repeated call cannot duplicate anything, and standard instrumentation so the next duplicate is answered from telemetry instead of an engineer attaching a debugger to a batch job.
## The wrong instinct and the right one The wrong instinct is to put `breakpoint()` at the top of the loop and press `n` until something looks wrong. With a 6,800-row batch that is thousands of prompts, and it answers the wrong question anyway. A duplicated side effect is not a question about the loop; it is a question about *who called the effect, twice*. So the breakpoint belongs on the effect, and the interesting information is the **stack** at each firing. ## Making the breakpoint selective `pdb` gives three ways to stop on one row out of thousands. 1. **A condition on the breakpoint.** `break dispatch.py:120, row_id == 6789` sets a breakpoint at that file and line that only fires when the expression is true *in that frame*. The expression is evaluated on every pass, so it must be cheap and must only reference names visible there. 2. **`condition` after the fact.** `break` with no arguments lists breakpoints with their numbers; `condition 1 row_id == 6789` attaches or replaces the condition on breakpoint 1, and `condition 1` with no expression removes it. This is the command you use when you already stopped and now know what to filter on. 3. **`ignore`.** `ignore 1 5000` skips the next 5000 hits of breakpoint 1 — the right tool when you know roughly how far in the problem starts but cannot express it as a predicate. `tbreak` sets the same thing as a one-shot breakpoint that removes itself after firing, and `commands 1` opens a small script attached to a breakpoint — a sequence of commands run automatically each time it fires, ending with `continue` if you want the program to keep going. `commands` plus `where` turns the debugger into a targeted stack sampler: every time the effect happens, you get a stack print, and the program runs on. ## Reading the stop When the breakpoint fires: * `where` (`w`, `bt`) prints the full stack, current frame marked. This is the single most valuable output for a duplicated-call bug. * `up` moves the *inspection* frame one level toward the caller; `down` moves back. Neither executes anything — the program is still suspended at exactly the same place. This trips people up: `up` is not "return". * `p expr` and `pp expr` evaluate arbitrary expressions in the frame you are currently inspecting, so after `up` you are reading the caller's locals. * `args` (`a`) prints the current function's arguments, which is usually the fastest way to see what was passed on this call versus the last one. For a duplicated effect, the routine is: stop on the first firing, `where`, copy the stack; `continue`; stop on the second firing, `where`, compare. Two different stacks mean two call sites — often a retry wrapper plus the original path, or a handler registered twice. Identical stacks mean one call site invoked twice — a loop that does not `break`, an iterator consumed twice, or a caller invoked once per listener. ## Entering the debugger where you need it If you can edit the source, a guarded call is precise and cheap: `if row_id == 6789: pdb.set_trace()`, or the same guard on `breakpoint()` so a deployment environment can disable it. Guarding on an environment variable as well means the line can survive a review. If you cannot edit the code — the function lives in a dependency — `pdb.runcall(func, *args, **kwargs)` calls it under debugger control and stops at its first line. `pdb.run("expr")` does the same for a statement string. Both are how you get a prompt inside code you do not own without patching it. ## The judgement part Interviewers at this level are listening for the constraints you volunteer. * **Do not do this on production traffic.** `pdb.set_trace()` suspends only the thread that hit it, while other threads keep running and holding locks — a paused worker in a live pool degrades the pool rather than freezing time. * **Reproduce it small first.** A batch of 6,800 rows that fails on one is a data-shaped bug. Extract that row, run the job over a one-row batch, and now you have a single-threaded reproduction where stepping is actually pleasant. * **Know when to stop debugging and start instrumenting.** If the duplicated effect is timing-dependent or lives across processes, no interactive session will show it; a counter keyed by row id, or a log line at the effect with the stack attached, answers the same question without a prompt. * **A conditional breakpoint costs time on every pass.** Evaluating a predicate 6,800 times is fine; evaluating one in a tight inner loop of millions is not, and `ignore` is cheaper. The strongest answers end with a fix that is not a debugger command at all: make the effect idempotent, key it by row id, so that a second call cannot duplicate it even if the second call survives.
- What is the difference between up and return once the debugger has stopped?up changes only which frame you are inspecting; the program stays suspended exactly where it stopped, and p now evaluates in the caller's namespace. return actually resumes execution, running the rest of the current function and stopping just before it returns. One is a read operation, the other advances the program — mixing them up is how people lose the state they were about to inspect.
- Why can attaching a condition to a breakpoint be worse than an ignore count?The condition expression is evaluated in the frame every single time the line is reached, so a breakpoint on a hot inner line pays that cost on every pass and can slow a job by orders of magnitude. An ignore count is just a decrement, so when you can express the target as "skip the first N hits" it is much cheaper. Conditions are for precision, ignore counts for volume.
- The duplicated effect only appears when the batch runs with several worker threads. Is pdb still the right tool?Usually not. set_trace suspends only the thread that reaches it while the others keep running, so the state you inspect is not a coherent snapshot, and the pause itself changes the timing you are trying to observe. Prefer instrumentation that records the effect with its row id and a captured stack, then reason offline; use the debugger afterwards on a single-threaded reproduction.
saying these in an interview costs you the question
- Proposes stepping through every iteration to find the row
- Sets the breakpoint on the loop instead of on the effect
- Thinks up resumes execution into the caller
- Ignores that only the calling thread is suspended
- Never inspects the stack of the second firing
- Attaches an expensive condition to a hot inner line