Skip to content

Edge cases and boundary conditions

coreCommon atGoogleStripeAnthropicMetaAmazonRead firstTesting and verificationPractise in the Sandbox →

Most bugs live at the edges of the input space: nothing, one thing, the same thing twice, the smallest and largest values, and the exact boundary of a range. Walk a fixed checklist per input type before you code, ask about the cases that need a decision, and write each one down as a named test. It costs about three minutes and is one of the clearest signals of seniority an interviewer sees.

A bridge inspector doesn’t test the middle of the span. They check where the bridge meets the bank, the joints, and what happens under the heaviest truck. Code is the same. The middle of the input space usually works. The edges are where loops run zero times, indices go out of range, and integers wrap around.

Testers call this boundary value analysis: split the inputs into groups that should behave the same (an equivalence class), then test at and just around each group’s boundary. Glenford Myers wrote it down in 1979 in The Art of Software Testing, and it still finds most bugs per minute spent.

The trick for interviews is to make it mechanical. You don’t need inspiration. You need a list per input type, which you run every time:

  • Size: empty, one, two, huge.
  • Values: zero, negative, duplicates, all equal, extremes (overflow).
  • Order: sorted, reverse sorted, unsorted when you assumed sorted.
  • Boundaries: exactly at a limit, one before, one after. Inclusive or exclusive?
  • Ties: two answers that are equally good. Which one wins?
  • Text: empty string, whitespace, case, unicode.

Pick an input type. Each card is an edge case, the bug it usually exposes, and whether it crashes, gives a wrong answer, runs too slow, or is a question you should ask first. Tick the ones your code handles, then copy them out as a table-driven test.

Edge case generator
tick and copy
input type
A list of integers: sums, maxima, counts, sorting. Tick each case once you are sure your code handles it (or you have asked the interviewer what it should do).
0 / 12 covered
Random small case
Small random inputs find the cases you didn't think of. Run your code on a few by hand.
As a table-driven test (all cases; tick some to narrow it)
import pytest


@pytest.mark.parametrize("xs", [
    pytest.param([], id="empty"),
    pytest.param([7], id="one element"),
    pytest.param([7, 3], id="two elements"),
    pytest.param([5, 5, 5], id="all equal"),
    pytest.param([3, 9, 9], id="duplicated maximum"),
    pytest.param([-4, -1, -9], id="all negative"),
    pytest.param([0, 0, 1], id="zeros"),
    pytest.param([1, 2, 3, 4, 5], id="sorted ascending"),
    pytest.param([5, 4, 3, 2, 1], id="sorted descending"),
    pytest.param([2147483647, 1], id="sum overflows int32"),
    pytest.param([-2147483648], id="int32 minimum"),
    pytest.param([1] * 100_000, id="large n"),
])
def test_edge_cases(xs: list[int]) -> None:
    ...  # assert your_function(xs) == expected

Always. But there are moments in a round when it pays the most:

The moment… What to do
right after hearing the problem Run the checklist out loud for the input type. Turn every “it depends” into a clarifying question.
before writing the loop Decide what empty and one-element input return. Write it as the first lines of the function, or explain why the general code already handles it.
the problem mentions time, ranges or windows Pin down inclusive vs exclusive at both ends. Write it in a comment.
the problem mentions money, counts or IDs Ask about size. If totals can pass 2³¹, use 64-bit integers in C# (or say why you don’t need to).
“return the top / best / first…” Ask for the tie-break rule before you sort.
you think you’re done Run the three smallest edge cases by hand. Then say what you’d still test with more time.
  1. Name the input types. A function f(intervals, now) has two: a list of pairs and a timestamp. Each type gets its own checklist.
  2. Run the checklist per type (the table below). For each line, decide: does it crash, give a wrong answer, or need a decision?
  3. Ask about the decisions. “If two players tie, who ranks first?” “Is an event exactly 60 seconds old still in the window?” Interviewers often leave these unspecified on purpose.
  4. Write the decisions down as a docstring or a comment at the top. This becomes your contract.
  5. Handle edges with a guard or with a general rule. A guard (if not xs: return None) is explicit. A general rule (start best at xs[0], use half-open ranges) makes the edge disappear. Prefer the general rule when it’s just as clear.
  6. Turn each case into a named test in a table. A failure then tells you which idea broke.
Input type Size Values Order / shape Boundaries and ties
Integer array [], [x], [x, y], 10⁵ items 0, negatives, all equal, duplicate max, int.MaxValue, int.MinValue sorted, reverse sorted k = 0, k = n, k > n
String "", one char, 10⁵ chars case, spaces, tabs, digits, leading zeros palindrome, all same char combining marks, emoji (UTF-16 length 2)
Intervals none, one negative coords, zero-length [4, 4], start > end unsorted, contained, duplicate touching [1,3]+[3,5]: closed or half-open?
Timestamps / windows no events t = 0, epoch milliseconds (> 2³¹) out of order, same tick event exactly at now - window, exactly at now, in the future
Graph no edges, one node self-loop, duplicate edge disconnected, cycle, very long path start = target, target unreachable
Grid [], [[]], 1×1 all walls, start blocked one row, one column, not square edge and corner cells (fewer neighbours)
Money / arithmetic zero amount negative, division by zero, overflow rounding half (2.5), remainders when splitting
Ranking / top-k fewer than k items negative scores all tied tie exactly at the k-th place

Example: a function that survives the checklist

Section titled “Example: a function that survives the checklist”

“Return the second largest distinct value” looks like a two-line problem. Run the integer-array checklist on it and five of the eight rows matter: empty, one element, all equal, duplicated max, all negative. The code below handles each one by construction, and the tests name them.

second_largest.py
def second_largest(xs: list[int]) -> int | None:
"""Second largest *distinct* value, or None if there isn't one."""
first: int | None = None # 1. "nothing seen yet" is None, not 0: 0 would break negatives
second: int | None = None
for x in xs:
if first is None or x > first:
first, second = x, first # 2. old max slides down to second
elif x != first and (second is None or x > second):
second = x # 3. skip duplicates of the max
return second
if __name__ == "__main__":
# One line per edge case, named so a failure tells you which one broke.
assert second_largest([]) is None # empty
assert second_largest([7]) is None # one element
assert second_largest([5, 5, 5]) is None # all duplicates
assert second_largest([3, 9, 9]) == 3 # duplicated max
assert second_largest([-4, -1, -9]) == -4 # all negative
assert second_largest([9, 3]) == 3 # max first (sorted descending)
assert second_largest([3, 9]) == 3 # max last (sorted ascending)
assert second_largest([2**63, 1]) == 1 # beyond 64-bit: fine in Python
print("ok")

The two choices that remove most edge cases:

  • “Nothing yet” is None/undefined/null, not 0. Starting first = 0 silently breaks the all-negative case. Starting at int.MinValue breaks when the input contains int.MinValue.
  • Duplicates of the max are skipped explicitly (x != first), because “second largest” in [3, 9, 9] should be 3.

Ranges and midpoints are where off-by-one bugs live. Two conventions remove most of them: half-open ranges [start, end) (start included, end excluded) and an overflow-safe midpoint.

boundaries.py
def overlaps(a_start: int, a_end: int, b_start: int, b_end: int) -> bool:
"""Half-open ranges [start, end): end is NOT included, so touching ranges don't overlap."""
# 1. An empty range [x, x) holds nothing. Without this guard, [3, 3) "overlaps" [1, 10).
if a_start >= a_end or b_start >= b_end:
return False
# 2. Otherwise they overlap unless one ends at or before the other starts.
return a_start < b_end and b_start < a_end
def midpoint(lo: int, hi: int) -> int:
"""Floor midpoint that can't overflow in fixed-width languages."""
# 3. lo + (hi - lo) // 2 never exceeds hi; (lo + hi) // 2 can overflow in C#/Java.
return lo + (hi - lo) // 2
if __name__ == "__main__":
assert overlaps(1, 5, 4, 8) # partial overlap
assert not overlaps(1, 5, 5, 8) # touching: 5 is not inside [1, 5)
assert overlaps(1, 10, 3, 4) # containment
assert not overlaps(3, 3, 1, 10) # empty range [3, 3) overlaps nothing
assert midpoint(0, 0) == 0
assert midpoint(2, 3) == 2 # floor, so the loop still makes progress
assert midpoint(-7, -2) == -5 # floor works for negatives too
print("ok")
Time in the round What it buys
Running the checklist out loud 1-2 min Clarifying questions that show judgment; no surprise follow-ups
Guards or general rules in code a few lines No crashes on the interviewer’s first test
Named edge-case tests 2-3 min Fast diagnosis when something fails; reusable at each new level
Skipping all of it 0 min now The interviewer finds the bug, and that costs more than the minutes

The real cost is attention, not time. A checklist turns it into routine, so the thinking goes into the problem.

  • Validate vs assume. In a library function, validate at the boundary and raise a clear error (ValueError, RangeError, ArgumentOutOfRangeException). In an algorithm round, state the assumption (“I’ll assume k ≥ 1”) and move on. Ask which one they want.
  • Sentinel vs optional. Returning -1 for “not found” is fine for indices. For values that can be any integer, return None / undefined / int? instead, so a real -1 isn’t confused with “nothing”.
  • Edges that are about time, not data. Calls in an unexpected order: return_book before checkout, pop on an empty queue, the same request twice. These are the edge cases of stateful APIs. See input validation and idempotency.
  • Scale as an edge. n = 10⁵ isn’t wrong, it’s slow: an O(n²) loop does 10¹⁰ steps, and recursion 10⁵ deep overflows the stack (Python’s default limit is 1000 frames).
  • Falsy zero. if x: / if (x) treats 0 (and "") as missing. Compare with None / undefined explicitly.
  • if where you needed while. “Evict the expired event” with if evicts one; a gap longer than the window needs all of them gone.
  • Fixed-width overflow. int sums, (lo + hi) / 2, Math.Abs(int.MinValue), and int.MinValue / -1 all overflow in C#. JS integers silently lose precision past 2⁵³.
  • Division semantics. -7 // 2 is -4 in Python but -7 / 2 is -3 in C#, and % follows different signs. See the toolkit.
  • String length is not characters. "👍".length is 2 in JS and C#; "é" can be one or two code points.
  • Fixing one edge by breaking the general case. A special-case if for [x] that returns early can skip work the general path needed. Re-run the normal tests after every guard you add.
  • After the problem statement: “Let me check a few edges: can the list be empty? Can values be negative? If two items tie, which comes first?” Ask only the ones that change the code.
  • Say the decisions out loud and write them at the top: “Empty input returns an empty list. Intervals are half-open, so touching ones don’t merge.”
  • When done, trace the smallest cases: empty, one element, two elements in both orders.
  • Expect the follow-up “What inputs would break this?” Answer with the checklist, not with a guess.
Check yourself
pick one
You merge meeting intervals. The interviewer gives [[1, 3], [3, 5]] and expects two separate meetings. What is the most useful thing to do?
Check yourself
pick one
A running maximum starts with best = 0 and passes every test you wrote. Which single input is most likely to break it?
Check yourself
pick one
In C#, which expression is safe for any two non-negative int indices lo <= hi?

Run the checklist on each before writing a line, then write the table of named cases:

  • Return the index of the first non-repeating character in a string.
  • Rotate an array right by k places (k can exceed the length, or be 0).
  • Given sorted, possibly duplicated integers, return how many times x appears.
  • Parse "HH:MM" strings and return the minutes between two times (what about crossing midnight?).
  • Split a bill of amount cents between n people so the shares differ by at most one cent.