Inputs, Outputs, Constraints, and Edge Cases
Specify exactly what a solution takes in, what it produces, and which unusual inputs it must still handle correctly — before writing any code.
By the end of this lesson, you can
- Specify a problem's inputs, outputs, and constraints before attempting a solution
- Identify edge cases a solution must handle correctly, not just the typical case
- Explain why a solution that only handles typical inputs can still contain a hidden logic error
Why it matters
Decomposing a problem tells you what pieces to build. Before building any of them, it pays to specify exactly what each piece takes in, what it’s supposed to produce, and — critically — which unusual inputs it still has to get right. Skipping this step is how a program ends up working perfectly on every input someone happened to try, and silently wrong on the first one nobody thought of.
Mental model
For any problem, write down four things before implementing it:
- Inputs — what comes in, and its type or shape
- Outputs — what comes out, and its type or shape
- Constraints — guarantees or restrictions on the input (always positive? always sorted? never empty?)
- Edge cases — unusual or boundary inputs the solution must still get right: an empty collection, a single item, a duplicate, the smallest or largest allowed value
The edge cases are the part that’s easy to skip, because a typical input “just works” without ever forcing you to think about them.
Small example
Take a function that returns a list’s first and last item. (items[-1]
is Python’s shorthand for “the last item” — a negative index counts
backward from the end, so -1 is always the last position no matter
how long the list is.)
def first_and_last(items):
return items[0], items[-1]
print(first_and_last([1, 2, 3, 4]))(1, 4)That works for the typical case. Now specify it properly:
- Input: a list of any values
- Output: a pair — the first item and the last item
- Constraint: none stated (any list is accepted)
- Edge cases: an empty list, and a list with exactly one item
Neither edge case was tested by the example above.
Trace it
| Input | Expected behavior | What actually happens |
|---|---|---|
| [1, 2, 3, 4] | returns (1, 4) | (1, 4) — correct |
| [5] | returns (5, 5) — first and last are the same item | (5, 5) — correct, if intended |
| [] | unclear — the problem never said | IndexError: list index out of range |
The single-item case turns out fine once you think it through — but the
empty-list case exposes something the original specification never
answered: what should happen for an empty list? Without deciding that
up front, the function’s behavior there is really just an accident of
how items[0] happens to fail.
Check your understanding
Or reveal the answer without checking
Answer:An empty list
An empty list forces a division by zero (0 items to divide the sum by) — a boundary condition that never comes up while testing with ordinary, non-empty lists.
Or reveal the answer without checking
Answer:Deciding a solution's intended behavior for an edge case is a design decision — finding it accidentally, as a crash, means the decision was never actually made
Specifying behavior in advance means you consciously decided what an empty list, a single item, or a boundary value should do — rather than whatever happens to occur once someone tries it and it breaks.
Practice: warm-up
For this problem, write down its inputs, outputs, constraints, and at least two edge cases: “Given a list of exam scores, return the highest score.”
Stuck? Reveal one hint at a time.
Hint 1
What should happen if the list has only one score? That's a legitimate edge case even though it feels trivial.
Hint 2
What should happen if the list is empty — is there a "highest score" of nothing?
Reveal one reasonable specification
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
Input: a list of numbers (exam scores)
Output: a single number (the highest score)
Constraints: none stated — scores could include duplicates or be in any order
Edge cases:
- a list with exactly one score (the answer is that score)
- an empty list (undefined by the problem — needs a decision: error, or None?)
- all scores tied (the answer is just that shared value)Practice: apply it
This function is meant to report which word in a list has the most vowels:
def most_vowels(words):
best_word = None
best_count = 0
for word in words:
vowel_count = 0
for letter in word:
if letter in "aeiou":
vowel_count = vowel_count + 1
if vowel_count > best_count:
best_word = word
best_count = vowel_count
return best_word
Or reveal the answer without checking
Answer:It returns None, since best_word is never reassigned — arguably reasonable, but only if that was actually decided on purpose
With no words, the outer loop body never runs, so best_word stays None — its starting value. That's not a crash, but whether None is the intended answer for 'no words at all' is exactly the kind of decision this lesson says to make deliberately, not by accident.
Modification challenge: decide what most_vowels([]) should
return, then add a check at the top of the function that makes that
decision explicit instead of leaving it to happen by accident.
Summary
- Before implementing, specify a problem’s inputs, outputs, constraints, and edge cases explicitly.
- An edge case is a boundary or unusual input — empty, single-item, duplicate, extreme value — not the typical case a first attempt is usually tested against.
- A solution that only handles typical inputs can still contain a hidden logic error, waiting for whichever edge case nobody thought to specify.
- Deciding a solution’s behavior for an edge case in advance is a design decision; discovering it as a crash later means that decision was never actually made.