The interviewer is deciding one thing: would they merge your code without checking it. Finding your own bug, out loud, answers that better than writing perfect code by luck.
Most candidates write the code, look at it, and say “I think that works”. That sentence transfers the job of verification to the interviewer, and they notice. This page is a repeatable way to verify code you cannot run.
Four rules make this reliable rather than theatre.
| Rule | Why |
|---|---|
| Write the table down | Holding six variables in your head is where the error hides. |
| Use a small input | Three to five elements. Enough to hit the loop twice, short enough to finish. |
| Include the loop condition as a column | Most bugs are in when the loop stops, not what it does. |
| Do not skip “obvious” lines | The obvious line is the one you typed wrong. |
A trace table for a two-pointer scan looks like this. Every column is a variable that changes.
| iter | left | right | state | best | branch taken |
|---|---|---|---|---|---|
| start | 0 | 4 | 0 | 0 | — |
| 1 | 0 | 3 | 7 | 7 | too big, right-- |
| 2 | 1 | 3 | 5 | 7 | too small, left++ |
After the dry run, check these five things specifically. They account for the large majority of interview bugs.
| # | Check | The question to ask |
|---|---|---|
| 1 | Before the first iteration | Are the initial values right? Is best seeded at 0 when it should be -inf or the first element? |
| 2 | The first iteration | Does anything read an index that does not exist yet, such as nums[i - 1] at i = 0? |
| 3 | The last iteration | Does anything read past the end, such as nums[i + 1] at the final index? |
| 4 | After the loop | Is there unfinished work? A stack still holding items, a final window never recorded, a last group never flushed? |
| 5 | The loop terminates | Does every path through the body advance something? This is where binary search hangs. |
Do not brainstorm from scratch under pressure. Run the list for the type you were handed.
| Input | Always test |
|---|---|
| Array | Empty. One element. Two elements. All identical. Already sorted. Reverse sorted. All negative. Contains zero. |
| String | Empty. One character. All the same character. A palindrome. Mixed case, if case matters. Spaces. |
| Linked list | None head. One node. Two nodes. The target is the head. The target is the tail. A cycle, if cycles are possible. |
| Tree | None root. One node. A left-only chain, which is the recursion-depth case. A perfect tree. Duplicate values. |
| Graph | No edges. A self-loop. A cycle. Disconnected components. Duplicate edges. A single node. |
| Grid | Empty grid. A single cell. One row. One column. All blocked. All open, which is the worst case for the stack. |
| Interval list | Empty. One interval. Touching endpoints. One fully nested inside another. Identical intervals. |
| Number | Zero. One. Negative. The maximum allowed by the constraints. Overflow, if the language has it. |
| k or a target | k = 0. k = 1. k = n. k > n. A target larger than the total. |
Six questions catch nearly all of them.
| Question | The usual answer |
|---|---|
| Is the range inclusive or exclusive? | range(n) excludes n. Slice a[i:j] excludes j. Say which you meant. |
What is the length of [left, right]? | right - left + 1. The + 1 is missed constantly. |
Should the loop be < or <=? | < when the two indices must differ. <= when a single element is a valid range. |
Is the array of size n or n + 1? | Prefix arrays and 1-indexed nodes both need the extra slot. |
Does mid round down or up? | Down. So left = mid can loop forever. See Pattern 11. |
| Does the answer include the current element? | Decides i versus i + 1 in backtracking, and prefix versus suffix in DP. |
Here is a real bug in a real solution, and how a dry run exposes it. The problem is Longest Substring Without Repeating Characters.
def longest_unique_buggy(s: str) -> int:
last_seen: dict[str, int] = {}
left = 0
best = 0
for right, ch in enumerate(s):
# BUG: no check that the previous
# sighting is still in the window.
if ch in last_seen:
left = last_seen[ch] + 1
last_seen[ch] = right
best = max(best, right - left + 1)
return best
def longest_unique(s: str) -> int:
last_seen: dict[str, int] = {}
left = 0
best = 0
for right, ch in enumerate(s):
# Only a repeat INSIDE the current
# window can force the left edge in.
if ch in last_seen and last_seen[ch] >= left:
left = last_seen[ch] + 1
last_seen[ch] = right
best = max(best, right - left + 1)
return best
Both pass "abcabcbb", "bbbbb" and "pwwkew", which are the three examples the problem gives. The bug needs a character that repeats after falling out of the window. The shortest such input is "abba".
| right | ch | last_seen[ch] | left, buggy | left, fixed | note |
|---|---|---|---|---|---|
| 0 | a | — | 0 | 0 | |
| 1 | b | — | 0 | 0 | best is 2 |
| 2 | b | 1 | 2 | 2 | a real repeat, both move |
| 3 | a | 0 | 1 | 2 | the buggy version moves left backwards |
At the last row the buggy version sets left = 1, which is behind where it already was. It then computes a width of 3 - 1 + 1 = 3 and returns 3. The correct answer is 2.
left should be checked for ever moving backwards, and left = max(left, ...) is a defensive way to write it. Third, the shortest input that breaks it had four characters. Small adversarial inputs beat large random ones.Many interviews now use an editor you can actually execute. If so, spend two minutes on a test block. It is faster than a dry run and far more convincing.
def _check() -> None:
"""Table-driven tests: examples first, then the edges."""
cases: list[tuple[str, int]] = [
("abcabcbb", 3), # from the problem statement
("bbbbb", 1), # from the problem statement
("pwwkew", 3), # from the problem statement
("", 0), # empty
("a", 1), # single character
("abba", 2), # the repeat that left the window
("tmmzuxt", 5), # the same trap, one step longer
]
for text, expected in cases:
actual = longest_unique(text)
assert actual == expected, f"{text!r}: got {actual}, want {expected}"
_check()
| Habit | Why |
|---|---|
| Table-driven, not one call per case | Adding a case is one line, so you actually add them. |
| Put the input in the failure message | assert actual == expected alone tells you nothing when it fires. |
| Given examples first, edges after | If a given example fails, you misread the problem, not the code. |
| One comment per edge case | It shows the interviewer why that case is there. |
| Test the helper separately | A bug in lower_bound is much easier to see in isolation. |
Last step. Stop being the author and become the reviewer. Ask these six questions about your own code.
| Question | What it catches |
|---|---|
| Which line assumes the input is non-empty? | Missing guard clauses. |
| Which line assumes values are positive? | Windows, greedy shrinking, sum thresholds. |
| Which line assumes no duplicates? | Value-keyed maps, dedup logic, rotated binary search. |
| What is the largest this can recurse? | Python’s recursion limit, on deep trees and full grids. |
| Am I mutating something the caller owns? | nums.sort(), in-place swaps, grid marking. Say it, or copy. |
| Is there a hidden linear operation in this loop? | in list, slicing, min, max, pop(0), string concatenation. |