Senior Engineering Interview Handbook / Chapter 36
Debugging Live
An interview-round skill chapter on live debugging: reproduction, hypothesis formation, instrumentation, state inspection, binary narrowing, invariant checking, regression tests, and communicating discoveries under time pressure.
Preparing audio…
Audio edition
Debugging Live
Page tools
The first impossible state
Your implementation has passed the sample. Then the interviewer tries
"abba". You expect the longest substring without repeated characters to
have length 2; the function returns 3.
The failure is useful precisely because it is small. Keep it on the screen. Do not add three more cases, reread the whole solution aloud, or start changing conditions. Say what the machine has established:
“For
abba, the function returns3; I expect2. I’m going to keep that input fixed and find the first step where the window stops being valid.”
That sentence separates observation from diagnosis. It also lets the interviewer correct the expected value or the contract before you debug the wrong problem.
Suppose this is the code:
def longest_unique(s):
last_seen = {}
left = 0
best = 0
for right, char in enumerate(s):
if char in last_seen:
left = last_seen[char] + 1
last_seen[char] = right
best = max(best, right - left + 1)
return best
The sample "abcabcbb" returns 3, so staring at the final expression is
unlikely to help. The productive question is narrower:
At what exact state does the implementation first violate a rule the algorithm depends on?
Here that rule is the window invariant: after each iteration, the substring
from left through right contains no repeated character. A repair is
trustworthy only if it restores that rule, not merely the expected number for
one input.
Turn suspicion into a hypothesis
A hypothesis should expose itself to being wrong. “There is an off-by-one
error” is too broad; almost any edit can be made to fit it. The repeated a in
abba suggests something more precise:
“The final
amay use a previous position that is already outside the active window. If it does, the duplicate branch can moveleftbackward.”
Now you know which state can confirm or reject the claim: right, char,
left, and the prior index for that character. One temporary trace is enough:
print(right, char, left, last_seen.get(char))
It produces:
0 a 0 None
1 b 0 None
2 b 0 1
3 a 2 0
Stop at the last line. Before processing the final a, left is 2; the
active window contains only the second b. The earlier a at index 0 is
outside that window. Yet the update sets left to 1. A boundary that should
only advance has moved backward, and the new “window” contains both b
characters.
This is the first impossible state. The trace has done its work. Printing the whole dictionary, every substring, and the running result would add output but no stronger evidence.
When the environment makes execution awkward, write the same four values by hand. A debugger, a print statement, and a hand trace are interchangeable when they answer the same question. Tool fluency matters less than choosing state that can disprove your hypothesis.
Repair the rule
The narrow fix prevents an old occurrence from pulling the window boundary backward:
def longest_unique(s):
last_seen = {}
left = 0
best = 0
for right, char in enumerate(s):
if char in last_seen:
left = max(left, last_seen[char] + 1)
last_seen[char] = right
best = max(best, right - left + 1)
return best
The max is not a patch for abba. It expresses the missing constraint:
left never moves backward. A prior occurrence changes the boundary only when
that occurrence is still inside the active window.
Verification should preserve the path by which you learned this. Rerun
abba first and keep it as a regression. Then check a nearby case that used to
pass, such as abcabcbb, and a boundary case such as the empty string. Remove
the temporary trace before declaring the code finished.
This order matters. If you run only the sample after editing, you have not shown that the original failure was repaired. If you run only the failure, you have not checked whether the repair disturbed ordinary behavior.
Narrow a larger search space
Not every bug yields to one loop trace. The same reasoning works when the code has helpers, phases, mutable objects, or several possible boundaries. Choose a checkpoint where the state has a clear expected meaning:
- after parsing but before algorithmic work;
- after initialization and before the main loop;
- immediately before and after the suspect branch;
- at a helper’s arguments and return value; or
- where ownership passes between caller state and local state.
If the state is already wrong at the checkpoint, move earlier. If it is still valid, move later. Repeating that decision is binary narrowing: it cuts away a region of code that cannot contain the first contradiction. The split need not be numerically exact. It needs to divide the execution into a part whose state you trust and a part you do not.
In a breadth-first search that returns a path that is too long, for example, inspect the queue and distance at the point a node becomes visited. If those are correct, move toward neighbor generation or path reconstruction. If they are already wrong, move toward enqueueing. Do not inspect sorting code merely because it is nearby. The shortest-path invariant tells you which states can possibly explain the result.
Assertions can make checkpoints sharper than prints:
assert left <= right + 1
old_left = left
left = max(left, last_seen[char] + 1)
assert left >= old_left
An assertion is useful when you can state what must be true. It is noise when it merely confirms that execution reached a line.
Know when a local fix is dishonest
Successful narrowing often reveals an update-order error, a comparison boundary, a missed copy, late visited marking, or a reversed comparator. These deserve local repairs because the representation still supports the contract.
Sometimes the failing state exposes a deeper mismatch. A set cannot represent duplicate counts. Process-local memory cannot enforce ownership across server processes. Depth-first search does not become an unweighted shortest-path algorithm through a better conditional. Shared mutable state cannot safely serve independent calls if the contract requires isolation.
In those cases, rewriting the responsible representation is narrower in meaning than piling conditions onto it. Explain why:
“The trace shows that this is not an update-order bug. The representation loses information the contract requires, so a local condition cannot make it correct.”
Minimal debugging means the smallest change that repairs the cause. It does not mean the fewest edited characters.
Let the interviewer hear the evidence
Live debugging should be audible, but the interviewer does not need access to every private thought. Speak at transitions where the evidence changes:
- State the mismatch and freeze the reproducing input.
- Name the invariant or contract that the result contradicts.
- Offer one hypothesis and the state that would test it.
- Report what the inspection established.
- Connect the repair to the invariant and name the verification cases.
For the sliding-window bug, the whole recovery can sound like this:
“
abbareturns3; I expect2, so I’ll keep that case. The active window must contain no repeated character. I suspect an old index is movingleftbackward. On the finala,leftis2while the old index is0, and my update changesleftto1. I’ll make the boundary monotonic withmax, then rerunabba, the original sample, and the empty case.”
That is enough narration to make the reasoning inspectable. Apologizing, blaming the language, listing untested theories, or explaining the intended algorithm while the actual state disagrees all consume attention without advancing the diagnosis.
If a hypothesis fails, say so plainly: “That branch preserves the invariant, so the cause is later.” Rejecting a plausible theory with evidence is progress. It shows control more clearly than defending the first guess.
Rehearse the recovery, not a script
Take a correct solution you know well and introduce one meaningful defect: store a backtracking path without copying it, mark a BFS node visited on dequeue, reverse a heap’s eviction rule, or leave state on an object between two calls. Do not look at the repair first.
For each defect, produce one compact debugging record:
- the smallest input that reproduces the mismatch;
- expected and observed results;
- the invariant or contract that has been violated;
- one falsifiable hypothesis;
- the smallest state trace that tests it;
- the repair and the regression case that preserves the discovery.
Then explain the recovery aloud while the code is still failing. The aim is not to memorize five polished sentences. It is to become comfortable pausing at evidence instead of reaching immediately for an edit.
A failed test gives you a contradiction. Debugging is the work of following that contradiction back to the first state that cannot be true, then repairing the rule that made it possible. Once the code behaves again, a different task begins: looking beyond the known failure for risks the current tests have not yet exposed.
Related reading
Continue reading
Full table of contents