Coding Interview Patterns Module 1 – How to approach a coding round
Edge cases and tests before you code
A checklist of edge cases for coding rounds, integer limits across five languages, tests that catch plausible wrong solutions, and shrinking a failing input.
What you will learn
- Use a checklist: empty, single item, duplicates, negatives, extremes, overflow, unsorted input, Unicode
- Write tests that separate a correct solution from a plausible wrong one
- Simplify a failing input to the smallest one that still fails
Before you start
On this page
Many wrong answers in coding rounds are not wrong algorithms. They are right algorithms that break on an input nobody tried: an empty list, a repeated value, a sum that no longer fits. Edge cases are cheap to find before you write code and expensive to find after an online assessment has marked them wrong. This lesson gives you a checklist to run in the first minutes of a round, the integer limits of five languages, a way to tell whether your tests are any good, and a way to shrink a failing input until the bug is obvious. The DSA track’s lesson on testing algorithms covers stress tests and their random generators in more detail.
A checklist to run before you code
Go through it after you have clarified the problem, and turn every line that applies into a test case with its expected answer. Each line is there because of a bug it tends to expose.
- Empty input. No items, an empty string, a graph with no edges. It catches code that reads the first element without checking, and code that returns a made-up answer such as 1.
- One item, and two. The smallest inputs where a loop runs zero times or once. Off-by-one errors live here.
- All equal, and repeats. Every value the same, or the largest value appearing twice. It catches code that assumed distinct values.
- Already sorted, sorted backwards, and unsorted. It catches code that silently assumed an order, and it is often the worst case of a brute force.
- Zero and negative values. It catches code that uses 0 to mean “nothing yet”, or assumes every value is positive.
- The extremes. The smallest and largest values and sizes the limits allow. Large sizes expose slow code; large values expose overflow, which the next section covers.
- The output’s corners. No answer at all (what do you return?), several right answers (does any of them count?), and ties.
- Text. The empty string, spaces, upper and lower case, and characters outside plain ASCII, such as accented letters and emoji, whose length depends on the language, as shown below.
You will not test every line every time. Pick the ones the problem’s input can produce, and say them aloud: listing edge cases before coding is part of what the testing area of an interview rubric looks for.
Integer limits differ by language
A sum of 200,000 values of up to 10⁹ each needs 64 bits. What happens when it gets only 32 depends on the language:
- Python: integers have unlimited precision, according to its documentation, so the sum is exact. The cost is speed, not correctness.
- Java:
intis 32 bits andlong64. The language specification says that integer operators “do not indicate overflow or underflow in any way”: the result silently wraps.Math.addExactand the otherExactmethods throw anArithmeticExceptioninstead. - Go: signed integers also wrap silently; the specification says overflow does not cause a run-time panic.
- C++: signed overflow is undefined behaviour under the standard’s rules for arithmetic, so nothing about the
result can be relied on; it may even look right. Compile with
-fsanitize=undefinedwhile practising, and Clang’s UndefinedBehaviorSanitizer reports signed overflow when it happens. Start a sum at a 64-bit value (0LL, or along longvariable) rather than at theint0. - JavaScript: numbers are 64-bit floating point, so every whole number is exact only up to
Number.MAX_SAFE_INTEGER, which is 2⁵³ − 1. Beyond it some integers cannot be stored, and two neighbouring integers can end up as the same number.BigIntis exact at any size, and anInt32Arraykeeps 32-bit integers that wrap like Java’s.
Both programs below add three readings of 2,000,000,000. Python shows the exact total and, for comparison, what a
wrapping 32-bit integer would end up holding; JavaScript shows the same wrap for real, in an Int32Array:
Python · limits/limits.py
# Python's integers grow as needed, so this sum is exact.
# A 32-bit int in Java, C++ or Go could not hold it.
readings = [2_000_000_000, 2_000_000_000, 2_000_000_000]
total = sum(readings)
print("Exact total:", f"{total:,}")
print("Largest 32-bit int:", f"{2**31 - 1:,}")
def as_int32(value):
"""The value a 32-bit two's-complement int ends up holding: keep the low 32 bits."""
value &= 0xFFFF_FFFF
return value - 2**32 if value >= 2**31 else value
print("What a wrapping 32-bit int would hold:", f"{as_int32(total):,}")
print("Largest 64-bit int:", f"{2**63 - 1:,}") Output
Exact total: 6,000,000,000 Largest 32-bit int: 2,147,483,647 What a wrapping 32-bit int would hold: 1,705,032,704 Largest 64-bit int: 9,223,372,036,854,775,807
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 limits.py
JavaScript · limits/limits.mjs
// JavaScript numbers are 64-bit floating point: every whole number is exact only up to 2^53 - 1.
console.log('Number.MAX_SAFE_INTEGER:', Number.MAX_SAFE_INTEGER);
console.log('2 ** 53 + 1 === 2 ** 53:', 2 ** 53 + 1 === 2 ** 53);
// An Int32Array keeps 32-bit integers, which wrap around the way a Java int does.
const totals = new Int32Array(1);
for (const reading of [2_000_000_000, 2_000_000_000, 2_000_000_000]) totals[0] += reading;
console.log('Sum kept in an Int32Array:', totals[0]);
// BigInt is exact at any size.
const exact = [2_000_000_000n, 2_000_000_000n, 2_000_000_000n].reduce((a, b) => a + b, 0n);
console.log('Sum as a BigInt:', exact.toString()); Output
Number.MAX_SAFE_INTEGER: 9007199254740991 2 ** 53 + 1 === 2 ** 53: true Sum kept in an Int32Array: 1705032704 Sum as a BigInt: 6000000000
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node limits.mjs
Runs on this device, in your browser. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The wrapped value, 1,705,032,704, is what a Java int would hold after this sum: positive, plausible and wrong, with
no error at all. That is why overflow belongs on the checklist. The test that catches it is the one built from the
largest values the limits allow.
Text whose length surprises you
The length of a string is another edge case that changes with the language and with how the text was typed:
Python · text/lengths.py
import unicodedata
word = "café"
decomposed = unicodedata.normalize("NFD", word) # é as e followed by a combining accent
print(len(word), len(decomposed), word == decomposed)
print(unicodedata.normalize("NFC", decomposed) == word)
print(len("🙂"), "🙂"[::-1] == "🙂") Output
4 5 False True 1 True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lengths.py
JavaScript · text/lengths.mjs
const word = 'café';
const decomposed = word.normalize('NFD'); // é as e followed by a combining accent
console.log(word.length, decomposed.length, word === decomposed);
console.log(decomposed.normalize('NFC') === word);
// JavaScript strings count UTF-16 code units: an emoji outside the first 65,536 characters takes two.
console.log('🙂'.length, [...'🙂'].length); Output
4 5 false true 2 1
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node lengths.mjs
Runs on this device, in your browser. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The same word can be stored with a single character for é, or as e followed by a combining accent, so “café” has 4
or 5 characters and the two spellings are not equal until they are normalised (unicodedata.normalize in Python,
normalize in JavaScript). Python counts the emoji as one character, so reversing the string with [::-1] leaves
it whole (the True on the last line); JavaScript counts two, because the ECMAScript specification defines a string
as a sequence of 16-bit code units, and this emoji needs two of them. Spreading the
string into an array ([...'🙂']) counts code points instead, which here gives 1.
Tests that catch plausible wrong solutions
A test suite is good when it fails for code that is wrong. You can check that directly: write versions of your code with one plausible mistake each, and see which tests catch them. This idea, mutation testing, goes back to the late 1970s work of DeMillo, Lipton and Sayward on choosing test data.
Here is a small problem, the length of the longest stretch of equal neighbours, with a correct version and four variations: three with a plausible mistake, and one that only looks different.
flat/kill_matrix.py
from versions import VARIATIONS, longest_flat
TESTS = [ # (input, expected): hand-written before any code ran
([4, 4, 4, 1], 3), # 1: the stretch is at the start
([7], 1), # 2: one value
([1, 2, 2], 2), # 3: the stretch is at the end
([], 0), # 4: no values
([3, 1, 2], 1), # 5: all different
]
for number, (values, expected) in enumerate(TESTS, start=1):
print(f"test {number}: {values} -> {expected}")
print()
print(f"{'':21}" + "".join(f"{n:>3}" for n in range(1, len(TESTS) + 1)) + " caught by")
for version in [longest_flat, *VARIATIONS]:
failed = [n for n, (values, expected) in enumerate(TESTS, start=1) if version(values) != expected]
marks = "".join(f"{'F' if n in failed else '.':>3}" for n in range(1, len(TESTS) + 1))
caught = ", ".join(f"test {n}" for n in failed) or "no test"
print(f"{version.__name__:21}{marks} {caught}")
print("F: the test fails for this version") Output
test 1: [4, 4, 4, 1] -> 3
test 2: [7] -> 1
test 3: [1, 2, 2] -> 2
test 4: [] -> 0
test 5: [3, 1, 2] -> 1
1 2 3 4 5 caught by
longest_flat . . . . . no test
restart_at_zero . . F . . test 3
no_empty_check . . . F . test 4
compares_with_first . . F . . test 3
ties_too . . . . . no test
F: the test fails for this version
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 kill_matrix.py
flat/versions.py
"""The longest stretch of equal neighbours, such as 3 for [4, 4, 4, 1].
A correct version, then four plausible variations of it."""
def longest_flat(values): # the correct version
if not values:
return 0
best = run = 1
for previous, current in zip(values, values[1:]):
run = run + 1 if current == previous else 1
best = max(best, run)
return best
def restart_at_zero(values): # after a change, the new stretch counts 0 instead of 1
if not values:
return 0
best = run = 1
for previous, current in zip(values, values[1:]):
run = run + 1 if current == previous else 0
best = max(best, run)
return best
def no_empty_check(values): # forgets that an empty list has no stretch at all
best = run = 1
for previous, current in zip(values, values[1:]):
run = run + 1 if current == previous else 1
best = max(best, run)
return best
def compares_with_first(values): # compares each value with the first one, not with its neighbour
if not values:
return 0
best = run = 1
for current in values[1:]:
run = run + 1 if current == values[0] else 1
best = max(best, run)
return best
def ties_too(values): # also replaces the best on a tie: different code, never a different answer
if not values:
return 0
best = run = 1
for previous, current in zip(values, values[1:]):
run = run + 1 if current == previous else 1
if run >= best:
best = run
return best
VARIATIONS = [restart_at_zero, no_empty_check, compares_with_first, ties_too] Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Read the matrix one row at a time:
- Tests 3 and 4 do all the catching. Test 3, a stretch at the end that does not include the first value, catches two different mistakes. Test 4, the empty list, catches the missing check.
- Test 1, the obvious example, catches nothing. It is the case everyone writes first, and every version passes it. A test suite made only of obvious examples looks thorough and proves little.
- Nothing catches
ties_too, and nothing should. It replaces the best on a tie, which never changes the answer. A mutant like that is called equivalent: when a mutant survives, first ask whether it is actually wrong.
In an interview you will not write mutants, but you can ask the same question of each test in your head: which mistake would this one catch? If the answer is “none I can think of”, replace it with one from the checklist.
Shrink a failing input
A stress test, like the one in the previous lesson, often fails on an input too long to trace by hand. Before debugging, shrink it: delete one value at a time, keep any deletion after which the input still fails, and stop when no single deletion does. This is a simple form of the delta debugging that Zeller and Hildebrandt described, which tries removing large chunks first and so usually needs far fewer tries on long inputs.
flat/shrink.py
import random
from versions import compares_with_first, longest_flat
def fails(values):
return compares_with_first(values) != longest_flat(values)
def shrink(values):
"""Delete values one at a time, keeping each deletion that still fails, until none does."""
shrinking = True
while shrinking:
shrinking = False
for i in range(len(values)):
smaller = values[:i] + values[i + 1 :]
if fails(smaller):
values, shrinking = smaller, True
print(" still fails:", values)
break # start again from the front of the shorter list
return values
rng = random.Random(3)
while True: # a stress test on long random lists, until one fails
values = [rng.randint(1, 3) for _ in range(12)]
if fails(values):
break
print("Failing list:", values)
print(f" longest_flat {longest_flat(values)}, compares_with_first {compares_with_first(values)}")
small = shrink(values)
print("Smallest failing list:", small)
print(f" longest_flat {longest_flat(small)}, compares_with_first {compares_with_first(small)}") Output
Failing list: [2, 2, 3, 1, 1, 3, 2, 3, 3, 2, 2, 3] longest_flat 2, compares_with_first 3 still fails: [2, 3, 1, 1, 3, 2, 3, 3, 2, 2, 3] still fails: [3, 1, 1, 3, 2, 3, 3, 2, 2, 3] still fails: [3, 1, 3, 2, 3, 3, 2, 2, 3] still fails: [1, 3, 2, 3, 3, 2, 2, 3] still fails: [3, 2, 3, 3, 2, 2, 3] still fails: [2, 3, 3, 2, 2, 3] still fails: [2, 3, 2, 2, 3] still fails: [2, 3, 2, 3] still fails: [3, 2, 3] Smallest failing list: [3, 2, 3] longest_flat 1, compares_with_first 2
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shrink.py
flat/versions.py
"""The longest stretch of equal neighbours, such as 3 for [4, 4, 4, 1].
A correct version, then four plausible variations of it."""
def longest_flat(values): # the correct version
if not values:
return 0
best = run = 1
for previous, current in zip(values, values[1:]):
run = run + 1 if current == previous else 1
best = max(best, run)
return best
def restart_at_zero(values): # after a change, the new stretch counts 0 instead of 1
if not values:
return 0
best = run = 1
for previous, current in zip(values, values[1:]):
run = run + 1 if current == previous else 0
best = max(best, run)
return best
def no_empty_check(values): # forgets that an empty list has no stretch at all
best = run = 1
for previous, current in zip(values, values[1:]):
run = run + 1 if current == previous else 1
best = max(best, run)
return best
def compares_with_first(values): # compares each value with the first one, not with its neighbour
if not values:
return 0
best = run = 1
for current in values[1:]:
run = run + 1 if current == values[0] else 1
best = max(best, run)
return best
def ties_too(values): # also replaces the best on a tie: different code, never a different answer
if not values:
return 0
best = run = 1
for previous, current in zip(values, values[1:]):
run = run + 1 if current == previous else 1
if run >= best:
best = run
return best
VARIATIONS = [restart_at_zero, no_empty_check, compares_with_first, ties_too] Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Twelve values became three: [3, 2, 3]. Now the bug in compares_with_first is easy to see: it compares each value
with the first one, so the two 3s count as a stretch although they are not neighbours. Keep the shrunk input as a
named test case, and the bug cannot come back unnoticed.
Key takeaways
- Run the checklist before you code: empty, one and two items, repeats, order, zero and negatives, extremes, the output’s corners and text.
- Integer limits differ: Python’s integers have no limit, Java and Go wrap silently, signed overflow in C++ is undefined behaviour, and JavaScript numbers hold every whole number exactly only up to 2⁵³ − 1.
- A good test catches a plausible mistake. If no mistake you can think of fails a test, replace it.
- Shrink a failing input before debugging it, then keep the small version as a test.
Exercise
Exercise · Easy · Python
Write tests that catch six wrong solutions
This time you write the tests, not the solution. The function under test is second_warmest(temps): given a list of whole-number temperatures in °C, it returns the second highest distinct temperature, or None when there are fewer than two distinct values.
second_warmest([31, 28, 35]) # 31
second_warmest([35, 35, 28]) # 28: 35 counts once
second_warmest([12]) # None
In warmest_tests.py, fill the list TESTS with up to 8 test cases, each a pair (temps, expected). The sample tests then:
- check that every one of your tests is right, using a correct second_warmest; - run your tests against six plausible wrong solutions, which you can read in the sample test file, and count how many of them at least one of your tests catches. A wrong solution is caught when it returns something other than expected, or raises an error, on one of your tests.
Use the lesson's checklist rather than the wrong solutions themselves: think of the inputs that break code like this, then check that each wrong solution falls to one of them.
Starter code · warmest_tests.py
# Up to 8 test cases for second_warmest(temps), each a pair (temps, expected).
TESTS = [
# ([31, 28, 35], 31),
] The sample tests · test_warmest_tests.py
from warmest_tests import TESTS
def second_warmest(temps):
"""A correct version: the second highest distinct value, or None."""
distinct = sorted(set(temps))
return distinct[-2] if len(distinct) >= 2 else None
# Six plausible wrong solutions. Each one is right on [31, 28, 35].
def sorted_second(temps): # forgets that the highest value may repeat
return sorted(temps)[-2]
def zero_means_nothing(temps): # uses 0 for "no value yet", which a real temperature can be
first = second = 0
for t in temps:
if t > first:
first, second = t, first
elif first > t > second:
second = t
return second if second != 0 else None
def single_value_back(temps): # returns the only value when every temperature is the same
distinct = sorted(set(temps))
return distinct[-2] if len(distinct) >= 2 else (distinct[0] if distinct else None)
def repeat_overwrites(temps): # a later repeat of the highest value overwrites the second
first = second = None
for t in temps:
if first is None or t > first:
first, second = t, first
elif second is None or t > second:
second = t
return second
def assumes_highest_last(temps): # treats the list as if it were in order, with the highest value last
return max(temps[:-1]) if len(set(temps)) >= 2 else None
def no_empty_list(temps): # max() of an empty list raises ValueError
top = max(temps)
rest = [t for t in temps if t != top]
return max(rest) if rest else None
def caught(wrong):
"""True when one of your tests makes the wrong solution return something else, or raise an error."""
for temps, expected in TESTS:
try:
if wrong(list(temps)) != expected:
return True
except Exception:
return True
return False
def test_format():
"""TESTS holds 1 to 8 pairs (temps, expected)"""
assert isinstance(TESTS, list)
assert 1 <= len(TESTS) <= 8, f"write 1 to 8 test cases; there are {len(TESTS)}"
for case in TESTS:
assert isinstance(case, tuple) and len(case) == 2, f"each test case is a pair (temps, expected), not {case!r}"
def test_tests_are_right():
"""every test case gives the answer a correct second_warmest gives"""
for temps, expected in TESTS:
assert second_warmest(list(temps)) == expected, f"second_warmest({temps!r}) is {second_warmest(list(temps))!r}"
def test_catches_sorted_second():
"""catches a version that forgets the highest value may repeat"""
assert caught(sorted_second)
def test_catches_zero_means_nothing():
"""catches a version that uses 0 to mean 'no value yet'"""
assert caught(zero_means_nothing)
def test_catches_single_value_back():
"""catches a version that returns the only value when all are equal"""
assert caught(single_value_back)
def test_catches_repeat_overwrites():
"""catches a version in which a later repeat of the highest overwrites the second"""
assert caught(repeat_overwrites)
def test_catches_assumes_highest_last():
"""catches a version that assumes the list is in order"""
assert caught(assumes_highest_last)
def test_catches_no_empty_list():
"""catches a version that fails on an empty list"""
assert caught(no_empty_list) A hint
Go through the checklist one line at a time: an empty list, a single value, all values equal, the highest value repeated, values that are negative or zero, and a list that is not in order. Each line is one test case.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- The Java Language Specification, Java SE 25: integer operations (Oracle)
- Math.addExact() (Java SE 25 API) (Oracle)
- The Go Programming Language Specification: integer overflow (The Go Project)
- C++ working draft, [expr.pre]: the result of an arithmetic expression (ISO C++ committee (WG21))
- UndefinedBehaviorSanitizer (The LLVM Project)
- ECMAScript Language Specification: Number.MAX_SAFE_INTEGER (Ecma International (TC39))
- ECMAScript Language Specification: the String type (Ecma International (TC39))
- Int32Array (MDN Web Docs)
- BigInt (MDN Web Docs)
- Numeric types: int, float, complex (Python Software Foundation)
- unicodedata.normalize() (Python Software Foundation)
- Hints on Test Data Selection: Help for the Practicing Programmer (DeMillo, Lipton and Sayward, Computer 11(4)) (IEEE)
- Simplifying and Isolating Failure-Inducing Input (Zeller and Hildebrandt, IEEE Transactions on Software Engineering 28(2)) (IEEE)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress