Articles
Codility Bug-Fixing Tasks: Find the Defect Before Rewriting the Solution
Prepare for Codility bug-fixing tasks with a worked off-by-one example, focused tests, minimal patches, and a clear way to explain the fix. Learn to test fixes.
A program can compile, pass the obvious example, and still fail the smallest boundary case. That is the core of a Codility bug-fixing task: diagnose an existing solution before you change it. The most productive first question is not “What algorithm would I write?” but “Which input makes this code’s behavior diverge from the specification?”
Codility’s official task-library description says its Bug-fixing tasks provide snippets that run but produce incorrect output; the candidate is asked to repair the solution within a small change budget, often two or three lines depending on the task. Codility also says automated assessment can include correctness and scalability cases. That is a platform description, not a promise that a particular employer will select this format or give a fixed time limit. No candidate report is needed to explain the method below. The code exercise is original practice material, not a leaked Codility task.
The point is to explain the failing input and the minimal repair, not merely to submit a replacement program.
What does the task actually ask you to preserve?
Treat the existing function as a constrained artifact. Read the prompt’s input domain, output contract, and permitted edits before touching code. A function that “works” on the samples may still mishandle an empty array, a singleton, the last pair, duplicates, negative numbers, or a maximum-sized input. Sometimes the underlying algorithm is sound and only a comparison, loop bound, or state reset is wrong. A complete rewrite may violate the change limit and throw away correct behavior you had not understood.
Write a one-sentence specification in your own words. Identify what the code appears to compute, then compare that interpretation with the real contract. For each loop, name its invariant: what should be true before the first iteration and after each iteration? For each index, list the smallest and largest values it can reach. For each mutable variable, ask whether it carries state across iterations or calls. This disciplined read often reveals the defect faster than trying random inputs.
Codility’s assessment explanation describes compiling and testing submissions in its online environment and checking multiple cases. The practical inference is to use the available run/test feedback as evidence, while assuming the visible examples are incomplete. Do not infer a hidden test’s exact input from a score alone.
Work a one-line off-by-one bug
Consider this original illustrative exercise. The requirement is to count adjacent equal pairs. Thus [2, 2, 2] has two pairs: positions (0, 1) and (1, 2). The function below runs and produces plausible answers for some inputs.
def count_equal_neighbors(values):
count = 0
for i in range(1, len(values) - 1):
if values[i] == values[i - 1]:
count += 1
return count
Before reading the body again, choose a discriminating case. For [2, 2], the expected result is 1. range(1, 1) is empty, so the actual result is 0. That single case narrows the defect to a loop that never visits the last eligible index. For [2, 2, 2], the function visits only i = 1 and returns 1 instead of 2. By contrast, an empty list and a singleton both correctly return 0, so testing only those cases would miss the problem.
The smallest repair changes one line:
for i in range(1, len(values)):
Now the loop visits exactly the indices whose left neighbor exists: 1 through len(values) - 1. It runs zero times for an empty list or singleton. It counts the final pair without ever reading beyond the list. This is stronger than saying “the output looks right”: the loop range matches the definition of an adjacent pair.
| Input | Expected | Before patch | After patch | What it probes |
|---|---|---|---|---|
[] | 0 | 0 | 0 | Empty input |
[7] | 0 | 0 | 0 | No eligible pair |
[2, 2] | 1 | 0 | 1 | Minimum failing length |
[2, 2, 2] | 2 | 1 | 2 | Final pair and overlap |
[2, 3, 3] | 1 | 0 | 1 | Equal pair only at end |
[2, 3, 2] | 0 | 0 | 0 | Equal values that are not adjacent |
The table is a compact explanation you could give during an interview. It records the contract, the counterexample, and why a one-line fix is sufficient. It also separates a regression check from a test that actually exposed the defect.
How do you find the smallest failing case?
Start with the structure implied by the task. For indexing code, test lengths zero, one, two, and three; also test the first and last positions. For numeric code, test zero, one, a negative value if allowed, and a value near a boundary. For sorting or deduplication, test equal elements, already-sorted elements, reverse order, and one repeated element. For stateful code, repeat a call or a value and see whether previous state contaminates the next result.
Use a differential oracle when you can write a tiny, obviously correct version of the specification for small inputs. For the pair-counting example, a reference could sum values[i] == values[i - 1] for i in 1..n-1. Generate short arrays over a tiny alphabet and compare the given implementation to the reference. The oracle is a diagnostic tool, not your submitted rewrite. If the tool environment permits it, enumerate all arrays of length up to four over {0, 1}. A failure that shrinks to [0, 0] explains more than a random thousand-element counterexample.
If writing a reference would reproduce the same conceptual mistake, instead derive expected outputs by hand from a table. State what each test proves. A test suite full of big arbitrary arrays gives weak localization. The strongest early tests differ by one feature: pair at the first boundary, pair at the last boundary, pair in the middle, no pair. This makes a wrong result causal rather than mysterious.
Trace the existing algorithm before editing
A useful trace records four columns: current iteration, values read, state before, and state after. In the example, list the values of i that Python’s range actually produces. It is easy to read range(1, len(values) - 1) as “up to the last index,” but its stop value is excluded. The trace corrects that intuition without speculation.
For more complex bug-fixing prompts, inspect each transformation boundary. If a function parses input, builds an intermediate structure, then aggregates a result, capture a representative value after each stage. Locate the earliest stage that diverges from the specification. A wrong final number could arise from parsing, ordering, mutation, or aggregation; modifying the last line because it is nearest the output is often the wrong move.
State assumptions aloud or in a short note: “I expect every legal pair to be visited exactly once; with two items the current loop has zero iterations.” That sentence both predicts the observed bug and points to the edit. It is more persuasive than “I tried a few things and this version passed.”
When is a tiny patch unsafe?
Minimal edits are a constraint, not a license to patch a symptom. Changing < to <= can fix one boundary and create an out-of-bounds read elsewhere. Reversing a sort order might fix a sample and break tie handling. Reinitializing a variable inside the loop could remove unwanted carry-over while also erasing needed accumulation. Before accepting a patch, re-establish the invariant and run cases that were previously correct.
Also distinguish a local logic bug from a scalability problem. Codility describes both correctness and scalability assessment for supported tasks. If the prompt asks only for a small logic repair, an asymptotic rewrite may be out of scope. If the instructions explicitly permit a broader change and the existing approach cannot meet stated limits, then explain why a structural change is necessary. The task’s actual wording governs; the official library’s common pattern does not override a particular prompt.
Watch for type and representation traps. Integer overflow in a fixed-width language, float comparisons, Unicode indexing, and mutation of shared containers may all produce narrow failures. Identify the input that distinguishes two hypotheses. For instance, if you suspect overflow, test near the integer limit; if you suspect stale state, call the routine twice. A hypothesis without a discriminating test is just a guess.
What should you do with compiler and test feedback?
First separate compilation or runtime errors from wrong answers. A compiler failure after your edit is direct evidence about syntax or types, not about the original algorithm. A timeout suggests complexity or a nonterminating loop; a wrong answer suggests a contract mismatch, though either can mask the other. Record which input category you have actually tested and what the platform has revealed. Do not treat a green sample test as proof of hidden-case correctness.
When feedback is sparse, avoid a cycle of unprincipled edits. Return to the specification and generate one fresh counterexample targeted at your current hypothesis. Compare before and after behavior in the local test tool. If your patch changes many lines, review each line’s necessity. You may discover you fixed two things at once and no longer know which change mattered.
Keep an eye on submission mechanics: the correct file, function name, signature, return value, and required language version. A correct idea can fail if it prints instead of returns, changes the public interface, or relies on an unsupported library. The official platform documentation supports using its own compile/test environment, but it does not supply your employer’s exact grading criteria; follow the test instructions you actually receive.
How to explain the fix in an interview
A good explanation has four parts. State the failing input and expected output. Show the exact divergent operation. State the smallest change. Explain why the repaired invariant covers the whole allowed domain. For the example: “Two equal elements should count as one pair. The stop value in range is excluded, so the loop does not run for length two and always skips the final pair. I changed the stop to len(values). Every index from one to the last is now visited once, and empty and singleton inputs still take zero iterations.”
Then identify one regression test and one edge test. This is enough to show the fix was reasoned, not guessed. If the interviewer asks about complexity, the scan uses O(n) time and O(1) additional space. That answer belongs to this particular exercise; a real Codility bug-fixing task may have a different complexity target.
Practice that resembles the debugging decision
| Practice question | Why it fits this task |
|---|---|
| Debug and Explain Fixes | Reproduce a defect and defend the correction. |
| Debug and Extend Cursor Queries | Trace a stateful boundary and check regressions. |
| Analyze and debug Python utilities | Inspect existing code before changing it. |
| Debug Tensor Conversion Bugs | Diagnose a representation mismatch. |
| Debug and test a Python function in venv | Use targeted tests to verify a fix. |
A useful rehearsal is to take one of these prompts and limit yourself to three stages: five minutes to find a failing case, five to articulate the invariant, and five to make and test the smallest justified patch. The timing is a practice constraint you control, not an official Codility duration. If you cannot explain why a change works beyond the sample, keep investigating before submitting.