Guides Guide 4 Process

Testing Your Own Code

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.

Contents

  1. The dry run
  2. The five boundary checks
  3. Edge cases by input type
  4. Hunting off-by-one errors
  5. A worked bug hunt
  6. Writing tests when you can run code
  7. The adversarial pass

The dry run

Pick the smallest input that exercises the interesting branch, then execute your code by hand, writing every variable into a table, one row per iteration. Do not read the code and imagine what it does. Execute it, literally, including the lines you are sure about.

Four rules make this reliable rather than theatre.

RuleWhy
Write the table downHolding six variables in your head is where the error hides.
Use a small inputThree to five elements. Enough to hit the loop twice, short enough to finish.
Include the loop condition as a columnMost bugs are in when the loop stops, not what it does.
Do not skip “obvious” linesThe 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.

iterleftrightstatebestbranch taken
start0400
10377too big, right--
21357too small, left++

The five boundary checks

After the dry run, check these five things specifically. They account for the large majority of interview bugs.

#CheckThe question to ask
1Before the first iterationAre the initial values right? Is best seeded at 0 when it should be -inf or the first element?
2The first iterationDoes anything read an index that does not exist yet, such as nums[i - 1] at i = 0?
3The last iterationDoes anything read past the end, such as nums[i + 1] at the final index?
4After the loopIs there unfinished work? A stack still holding items, a final window never recorded, a last group never flushed?
5The loop terminatesDoes every path through the body advance something? This is where binary search hangs.
Check 4 is the one people skip. Monotonic stacks leave items behind. Sliding windows can finish without recording the last valid window. Grouping loops need a final flush. If your loop accumulates anything, ask what happens to it when the input runs out.

Edge cases by input type

Do not brainstorm from scratch under pressure. Run the list for the type you were handed.

InputAlways test
ArrayEmpty. One element. Two elements. All identical. Already sorted. Reverse sorted. All negative. Contains zero.
StringEmpty. One character. All the same character. A palindrome. Mixed case, if case matters. Spaces.
Linked listNone head. One node. Two nodes. The target is the head. The target is the tail. A cycle, if cycles are possible.
TreeNone root. One node. A left-only chain, which is the recursion-depth case. A perfect tree. Duplicate values.
GraphNo edges. A self-loop. A cycle. Disconnected components. Duplicate edges. A single node.
GridEmpty grid. A single cell. One row. One column. All blocked. All open, which is the worst case for the stack.
Interval listEmpty. One interval. Touching endpoints. One fully nested inside another. Identical intervals.
NumberZero. One. Negative. The maximum allowed by the constraints. Overflow, if the language has it.
k or a targetk = 0. k = 1. k = n. k > n. A target larger than the total.
Say which ones you are skipping and why. “The problem guarantees at least one element, so I will not guard the empty case” scores better than silently not handling it. It shows you considered it and made a decision.

Hunting off-by-one errors

Six questions catch nearly all of them.

QuestionThe 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.
The fastest off-by-one test. Run the code on an input of size one, and then on size two. Almost every boundary error shows up in one of those two, and both take twenty seconds to trace.

A worked bug hunt

Here is a real bug in a real solution, and how a dry run exposes it. The problem is Longest Substring Without Repeating Characters.

Buggy
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
Fixed
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".

rightchlast_seen[ch]left, buggyleft, fixednote
0a00
1b00best is 2
2b122a real repeat, both move
3a012the 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.

Three lessons from this one bug. First, the given examples are not a test suite; they are the happy path. Second, a monotone variable such as 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.

Writing tests when you can run code

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()
HabitWhy
Table-driven, not one call per caseAdding a case is one line, so you actually add them.
Put the input in the failure messageassert actual == expected alone tells you nothing when it fires.
Given examples first, edges afterIf a given example fails, you misread the problem, not the code.
One comment per edge caseIt shows the interviewer why that case is there.
Test the helper separatelyA bug in lower_bound is much easier to see in isolation.
If you have time and the problem allows it, check against the brute force. Write the O(n²) version you already described in phase 3, generate a few small random inputs, and compare. That catches bugs no hand-picked case will, and interviewers rate it highly. It only works when the brute force is genuinely trivial to write, so judge the clock.

The adversarial pass

Last step. Stop being the author and become the reviewer. Ask these six questions about your own code.

QuestionWhat 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.
Say the answers out loud, even when they are fine. “I am sorting the input in place, which mutates the caller’s list. If that is a problem I would copy first.” That sentence takes four seconds and tells the interviewer you think about the code as something other people have to live with. That is most of what a senior signal is.

The five things to carry forward


Guide 3 — The Interview Script Back to all 16 patterns