Limited-time offer: 80% off every plan

See plans

Debugging Coding Interview Guide: Find Bugs Out Loud

A practical loop for debugging coding interview rounds: restate the failure, isolate the boundary, form one hypothesis, verify it, and narrate the fix honestly.

By Emily Miller14 min read

A debugging coding interview is not a trick-detection contest. You are handed code that misbehaves, and the emphasis is on method: can you reproduce the failure, narrow the search, state a hypothesis, test it, and explain the fix without guessing? The good news is that this is a learnable loop, and you can rehearse it out loud in an afternoon. Restate the failure, isolate the boundary, form one hypothesis, verify it, then state the fix and its side effects.

What a Debugging Coding Interview Actually Tests

In a from-scratch round, the interviewer watches you turn a problem statement into data structures and control flow. In a debugging round, the starting point is already there, and the questions change.

Method, not magic

Debugging rounds reward a search that converges, not a lucky spot. That means:

  • Reproducing the failure with a concrete input.
  • Isolating which region of the code could produce it.
  • Stating one hypothesis and the evidence that would confirm or kill it.
  • Verifying, then explaining the fix and its side effects.

A visible elimination process is often easier for an interviewer to follow than a fast guess you cannot explain. Build the habit of showing your search rather than racing to a patch.

Why your from-scratch prep does not fully transfer

When you write code yourself, you carry a mental model of every variable. Reading someone else's code is a different skill: you must build that model from the outside in. Practice problems that ask you to implement a function do not train you to ask "which line could have produced this wrong value?" So set aside time for pure reading practice, not just solving.

Narration is the deliverable

"This function should give the length of the longest repeat-free substring. On this input it returns one, but the expected answer is two. So the window is being shrunk too far somewhere." That single sentence tells the interviewer you have restated the contract, used the evidence, and localised the search. Silence often reads as stalling even when your thinking is excellent.

A Four-Step Loop You Can Say Out Loud

Use the same four steps every time. The repetition frees your attention for the actual bug, and it is the core habit of a debugging round.

Step 1: Restate the contract and the failure

Two sentences. What the code is supposed to do, and what it does instead, using the failing example as evidence. "This should measure the longest stretch of characters with no repeats. On the sample input it undercounts by one."

Step 2: Locate the boundary

Name the smallest region that could produce the wrong value: one function, one loop, one branch. You are not looking for the bug yet, only for where it can live. "The window's left edge only moves when a duplicate appears. The right edge advances every iteration. A result that is too small points at how the left edge is updated."

Step 3: Form one hypothesis

State a single specific claim and what would confirm it. "I think the left edge jumps past the previous occurrence instead of landing one position after it. If that is right, a two-character input with a repeat should undercount by exactly one."

Step 4: Verify, then fix and state side effects

Run the smallest test that separates your hypothesis from the alternatives. Then say the fix and what else it touches. "Changing the update to last_seen[ch] + 1 gives two. It does not change behaviour on the empty string, because the loop never runs."

Keeping the loop visible when you are wrong

A wrong hypothesis is not a failure; an unlabelled one is. When one dies, say the line that keeps your search legible: "That rules out the right edge, so the remaining suspect is how the left edge is updated." Then move to the next suspect.

Reading the Code Before You Change It

In a debugging coding interview, most of your time goes on reading, so read with a target. Pick one input value and follow it from entry to the wrong output. Where does it first differ from what you expect? That first divergence is your boundary. Tracing lines in order is slower and easier to get lost in.

Name the invariants

Before reading the body, write down what should always be true: the list stays sorted, counts are never negative, keys are unique, the window contains no repeats. Then find the line where the invariant breaks. Bugs cluster at invariant boundaries.

Common bug shapes

  • Off-by-one in a bound or an index update.
  • Wrong comparison operator, especially <= where < was meant.
  • A shared structure mutated when a copy was intended.
  • A cached or default value that is stale on the second call.
  • Early return that skips cleanup or a final update.

Read the tests first

Test names encode intended behaviour. A test called handles_single_element tells you the author expected that case to matter. If a test is failing, its input is the smallest reproduction you already have.

Worked Example: An Off-by-One in a Sliding Window

Here is a short function that should measure the longest stretch of characters with no repeats.

def longest_unique(s):
    last_seen = {}
    left = 0
    best = 0
    for right, ch in enumerate(s):
        if ch in last_seen and last_seen[ch] >= left:
            left = last_seen[ch]
        last_seen[ch] = right
        best = max(best, right - left + 1)
    return best

For "aba" it returns one; the expected answer is two.

Restate. The contract is the length of the longest repeat-free substring. The failure is an undercount by one.

Locate. The result is too small, so the window is being shrunk too far. The only shrinking line is left = last_seen[ch].

Hypothesise. The left edge should move to one position after the previous occurrence, not onto it. If the hypothesis is right, the current code returns one on "aba" and the fix will make it return two.

Verify. Choose a test that separates this hypothesis from a rival. A rival explanation is an off-by-one on the right edge, which would undercount on "ab" too, since no duplicate ever appears there. Run "ab" on the current code: it returns two, the correct answer, so the right edge is ruled out. Now run "aba": it returns one, which matches the left-edge prediction. The hypothesis survives and the rival does not.

Fix. Change the line to left = last_seen[ch] + 1. Now "aba" returns two and "abcabcbb" returns three. The empty string still returns zero, because the loop body never executes, so left stays at zero and best stays at zero.

The habit worth keeping: test the smallest failing case, not a large random string. "aba" isolates the bug in three characters.

Worked Example: A Wrong Result From a Shared Reference

This function is meant to group items by a key, but the groups come out wrong: every key maps to the same list object, so each group contains all the items.

def group_by(items, key):
    groups = {}
    bucket = []
    for item in items:
        groups.setdefault(key(item), bucket).append(item)
    return groups

One list object is created before the loop and reused for every key. The first time a key appears, setdefault stores that shared list under it; later items for any key append to the same list. So every key ends up pointing at one list holding all the items. The aliasing is the visible symptom of a deeper structural error: no per-key list is ever created.

Restate and locate. The output shape is right but the values are wrong and identical. The only place a list is created is outside the loop, so the boundary is the loop body.

Describe it in terms of state and lifetime. The bug is not "it needs a copy". It is that a single mutable object is created once and outlives every iteration, so every dictionary entry references that one object. Saying it that way shows you understand aliasing, not just the patch.

Fix. Create a fresh list per key: groups.setdefault(key(item), []).append(item). The setdefault call returns the existing list for a key, or inserts a new empty list and returns it, so each key gets its own list on first sight and later items append to that key's list.

Verify. Run it on [("a", 1), ("b", 2), ("a", 3)] with key=lambda pair: pair[0]. The result is {"a": [("a", 1), ("a", 3)], "b": [("b", 2)]}, and the two lists are distinct objects, so appending to one does not change the other.

Likely follow-up. If the structure were shared across threads, the same reasoning applies: shared mutable state needs either per-thread copies or explicit synchronisation. Say it in one sentence: "If two threads appended to the same key at once, the per-key list would need a lock or a per-thread copy, because setdefault does not make the append atomic." That names the risk and the fix before you are asked.

Talking About Time and Space While You Debug

Debugging fixes often change complexity. In a debugging coding interview, naming the complexity change unprompted is a habit worth building.

Suppose the original code scans a list inside a loop to check membership. That is quadratic in the number of items. Replacing the scan with a set lookup makes it linear in the expected or average case, at the cost of memory proportional to the set. Be precise about the worst case: with pathological collisions each lookup degrades to O(n), so the loop can return to O(n squared) worst case. Say exactly that: "This changes the loop from O(n squared) to expected O(n) with average-case hash lookups, and I am now holding a set of n items."

State complexity claims precisely:

  • Name the variable: n items, k distinct keys, m queries.
  • Name the case: worst case, average case, or best case.
  • Name what you are counting: comparisons, hash lookups, or memory cells.

If a slower correct fix is easier to verify, offer it first and name the trade-off, then offer the faster version. "I can fix this in place, which stays quadratic but is a two-line change. If you want linear, I would add a set and accept the extra memory." That is a stronger answer than silently optimising.

When You Are Stuck: Honest Moves That Still Score

Being stuck is normal. Being vague about it is what costs you.

  • Give a map. "I know the input is parsed correctly because the first print matches. I have ruled out the loop bound. The remaining suspect is the comparison inside the branch."
  • Ask a scoped question. "Should this function treat an empty list as valid input, or is that out of scope?" That is about intended behaviour, not about the answer.
  • Offer an instrumented fallback. "I would add a print of the accumulator at the end of each iteration and see where it first diverges." Then explain what each possible output would tell you.
  • Do not pretend. Claiming a fix you have not verified costs far more credibility than saying "the undercount is confirmed; I am still locating which update causes it."

If you are running low on time, summarise: what you know, what you ruled out, what you would check next, and the smallest test that would confirm it. A clear, honest partial result with a named next step is often easier to defend than a rushed patch you cannot justify.

Practising Debugging Out Loud

Reading about the loop is not the same as running it under mild pressure. Build a small practice habit, and treat each session as a debugging coding interview in miniature.

  1. Take a function you wrote a month ago and introduce one deliberate mistake: flip a comparison, move a list creation outside a loop, change an index by one.
  2. Set a five-minute timer and narrate your search aloud, using the four steps.
  3. Record yourself and check three things: did you state a hypothesis, did you verify it with a small input, and did you name the complexity of your fix?
  4. Swap with a study partner and debug each other's planted bugs. Hearing someone else's narration sharpens your own.

Rehearse the vocabulary of evidence until it is automatic: "this input shows", "that rules out", "the remaining suspect is". These phrases keep your reasoning legible even when you have not yet found the bug.

A practice tool can act as a rehearsal partner for structuring your reasoning out loud and reviewing your own explanation afterwards. Treat it as practice for your reasoning, never as a source of answers during a real interview, and keep your preparation within the rules of the employer and platform you are interviewing with. For related practice, see Python Interview Questions: Concepts and Trade-Offs, and browse our blog for more guides on reasoning out loud.

Before Your Next Round

A debugging coding interview rewards a visible method more than a fast guess. The takeaway: the loop is the answer, and you can carry it in on one sheet of paper. Before the round, write the four steps down and read them once, so that when the code lands in front of you, the first thing you say is the contract and the failure rather than a guess at the fix.

Frequently asked questions

What if the code is in a language I am less fluent in?

Read for structure first and ask about idioms you do not recognise. Most languages share the same shapes: loops, branches, collections, index updates. Say what you can see, for example: "I am not certain of this method's exact behaviour, but it looks like it returns the existing value or inserts a default." Asking a scoped question about an idiom is normal in a debugging coding interview and reads as care, not weakness.

Do I need to fix the bug to pass a debugging interview?

Not always, but a verified diagnosis plus a named next step is usually more persuasive than an unverified patch. Ask a scoped question about time or intended behaviour, for example: "Would you like me to keep tracing, or is a verified diagnosis with a proposed fix enough for now?" Then spend your remaining time on the smallest test that would confirm your hypothesis rather than on a rushed change you cannot justify.

What if I am unsure of the complexity of my own fix?

Say so plainly and give a bound you can defend. "I think this is linear in the number of items, because each one is visited once, but I would want to check whether the inner lookup is constant." Naming the part you are unsure about, and the test that would settle it, is stronger than a confident figure you cannot back up.

Is it acceptable to add print statements or write a small test during a debugging round?

Yes, as long as you explain what each one is meant to reveal. A targeted print or a two-character test input is evidence gathering. Say what outcome would confirm your hypothesis and what outcome would rule it out.

How is a debugging interview different from a normal coding interview?

You start from existing code instead of a blank editor, so the interviewer watches how you read, trace and narrow a search rather than how you build a solution from scratch. Writing new code tests construction; debugging tests diagnosis. Expect fewer questions about which data structure to choose and more about which line could produce the wrong value and how you would prove it.

What should I do if the interviewer gives no failing example?

Ask for one, or construct one yourself and say that you are doing so. "I will try a small input with a repeated character and see whether the result matches the contract." A self-made reproduction still gives you evidence, and stating it aloud keeps the interviewer inside your reasoning.

Practise saying your debugging loop out loud and review your reasoning.