Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot

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: int is 32 bits and long 64. The language specification says that integer operators “do not indicate overflow or underflow in any way”: the result silently wraps. Math.addExact and the other Exact methods throw an ArithmeticException instead.
  • 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=undefined while practising, and Clang’s UndefinedBehaviorSanitizer reports signed overflow when it happens. Start a sum at a 64-bit value (0LL, or a long long variable) rather than at the int 0.
  • 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. BigInt is exact at any size, and an Int32Array keeps 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:

One sum, exact and wrapped

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

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.

Programmer Calculator (Bitwise) See how a sum overflows a 32-bit or 64-bit integer, bit by bit.

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:

Two kinds of é, and an emoji

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

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.

Which test catches which mistake?

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]

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.

Shrinking a failing list

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]

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.

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.

  1. Question 1 of 6 A function keeps the best value so far in a variable that starts at 0. Which test is most likely to catch it?

    Choose one answer.

    Show the answer to question 1

    Answer: A list whose values are all negative

    Starting at 0 quietly assumes that some value is at least 0. With only negative values the variable never changes, and the function returns 0, a value that is not even in the list.

  2. Question 2 of 6 In Java, int total = 2_000_000_000; total += 2_000_000_000; runs. What is total afterwards?

    Choose one answer.

    Show the answer to question 2

    Answer: A negative number, because the sum wrapped around 32 bits without any error

    The Java Language Specification says that integer operators do not report overflow, so the result keeps the low 32 bits: 4,000,000,000 − 2³² = −294,967,296. Math.addExact would throw an ArithmeticException instead.

  3. Question 3 of 6 lengths.py (the Python tab) prints the lengths of "café" written two ways, whether they are equal, and facts about an emoji. What does it print?

    What does this program print? Choose one answer.

    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] == "🙂")
    Show the answer to question 3

    Answer: it prints

    4 5 False
    True
    1 True

    The decomposed spelling stores é as e plus a combining accent, so it has 5 characters and is not equal to the 4-character word until it is normalised back. Python counts the emoji as one character, and reversing a string of one character changes nothing.

  4. Question 4 of 6 A version of your code with one change passes every one of your tests. What should you ask first?

    Choose one answer.

    Show the answer to question 4

    Answer: Whether the change can ever give a different answer; if it can, a test is missing

    Some changes, like ties_too in the lesson, never change an answer: they are equivalent, and no test can catch them. If the change can give a different answer on some input, that input is the test you are missing.

  5. Question 5 of 6 While shrinking a failing list, you delete one value and the shorter list no longer fails. What next?

    Choose one answer.

    Show the answer to question 5

    Answer: Keep that value, and try deleting a different one

    That value is needed for the failure, so it stays. You stop only when no single deletion keeps the list failing. The shrinking in the lesson took a list of twelve values down to three this way.

  6. Question 6 of 6 compares_with_first compares each value with the first value instead of its neighbour. Which of these tests catch it?

    Choose every answer that is right.

    Show the answer to question 6

    Answer:

    • [1, 2, 2], whose longest stretch is 2
    • [3, 2, 3], whose longest stretch is 1

    The mistake only shows when the values that match the first one are not neighbours of it, or when the longest stretch does not include the first value. A stretch at the start, or a single value, looks the same either way.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.