Why this matters
An if/elif chain with no else does nothing when none of the conditions match. A match statement with no case _: does the same. A dictionary lookup with a key you did not plan for raises KeyError. A function whose last if does not fire returns None without anyone deciding that it should.
In each of these, the program has an input it never thought about, and its behavior for that input was never chosen. It just happens. That is the gap people find, by accident or on purpose. The user types a menu option you forgot. The file contains a status word you never defined. A value is None because a function fell off the end.
A program that covers all cases has decided, for every branch, what happens when nothing else matches. Usually that decision is “report the unexpected value and stop,” which is far safer than silently continuing.
The rule: Every if/elif chain ends in an else. Every match ends in case _:. Every function returns a value on every path or raises an exception. The default case handles the unexpected input deliberately, by rejecting it, instead of letting the program drift on.
Benefits
- The program fails loudly on input it does not understand, instead of doing something undefined.
- When you add a new option and forget to handle it somewhere, the default case tells you.
- Readers can see that you thought about the unexpected, which makes the code easier to trust.
- Tests can target the default case directly.
Red flags (violations)
- An
if/elifchain over a menu choice, a status, or a command with noelse. - A
matchstatement with no wildcard case. items[name]wherenamecame from the user, with no check and no.get().- A function that returns a value inside an
ifand nothing after it. - A default that quietly does the “safe” thing instead of saying something went wrong, such as
else: pass.
Example 1 - The menu with a missing branch
Problematic code
def run_choice(choice: str, items: dict) -> None:
if choice == "1":
add_item(items)
elif choice == "2":
remove_item(items)
elif choice == "3":
list_items(items)
The menu says there is a 4. Quit. The programmer forgot to handle it. Choose 4 and the loop just shows the menu again. If a teammate later adds option 5 to the menu text, the same thing happens.
Fixed with a default that complains
def run_choice(choice: str, items: dict) -> None:
if choice == "1":
add_item(items)
elif choice == "2":
remove_item(items)
elif choice == "3":
list_items(items)
elif choice == "4":
save_and_quit(items)
else:
raise ValueError(f"Unhandled menu choice: {choice!r}")
Why this is better: The missing branch is added, and the else makes sure the next missing branch cannot hide. Because ask_menu_choice() already validates input, the else should never run. If it does, a developer made a mistake, and raising is the right way to say so.
The same shape with match:
def run_choice(choice: str, items: dict) -> None:
match choice:
case "1":
add_item(items)
case "2":
remove_item(items)
case "3":
list_items(items)
case "4":
save_and_quit(items)
case _:
raise ValueError(f"Unhandled menu choice: {choice!r}")
Example 2 - A status word from a file
Problematic code
def restock_needed(item: dict) -> bool:
status = item["status"]
if status == "in_stock":
return False
elif status == "low":
return True
elif status == "out":
return True
The file was edited by hand and one line says backordered. The function returns None. None is falsy, so the caller treats it as “no restock needed,” and the item is never reordered. Nobody sees an error.
Fixed so every path returns or raises
RESTOCK_STATUSES = {"low", "out"}
KNOWN_STATUSES = {"in_stock"} | RESTOCK_STATUSES
def restock_needed(item: dict) -> bool:
status = item.get("status")
if status not in KNOWN_STATUSES:
raise ValueError(f"Unknown status {status!r} for item {item.get('name')!r}")
return status in RESTOCK_STATUSES
Why this is better: The known statuses are written down once. Anything else raises immediately, with the item name, so the bad line in the file gets fixed. item.get("status") returns None instead of raising KeyError when the key is missing, and None then fails the same check. Every path through the function returns a bool or raises.
Example 3 - Dictionary lookups
Problematic code
def quantity_of(items: dict, name: str) -> int:
return items[name]
A name that is not in the inventory raises KeyError, which is the right thing only if the caller expects it. Most callers do not.
Fixed by choosing the default on purpose
def quantity_of(items: dict, name: str) -> int:
"""Return the quantity on hand, or 0 if the item is not stocked."""
return items.get(name, 0)
Why this is better: The default of 0 is a decision, written in the docstring, and the caller can rely on it. Sometimes the right default is to raise; then keep items[name] and document that it raises KeyError. Either way, someone chose.
How to apply this rule
- Finish every chain. Add
elseto everyif/elifchain that selects among values, andcase _:to everymatch. - Make the default loud. The default should raise or print a clear message. It should not quietly skip or guess.
- Return on every path. If a function returns a value, every branch returns one or raises. Run the function in your head with an input you did not plan for and see where it ends up.
- Write the valid set down. A set like
KNOWN_STATUSESmakes the check one line and gives the default case something to compare against. - Use
.get()when a default makes sense. When a missing key has a sensible meaning, say so with.get(key, default). When it does not, letKeyErrorraise and document it.
Knowledge Check
- An
if/elifchain handles choices “1”, “2”, and “3” with noelse. The user enters “9”. What happens?- Python raises an error
- Nothing; no branch runs and execution continues after the chain
- The first branch runs by default
- The program exits
Answer
2 — with no matching condition and no `else`, the chain does nothing. The program continues as if the choice had been handled. - Why does a function that “falls off the end” cause trouble?
- Python forbids it
- It returns
None, which the caller may treat asFalse,0, or an empty value without noticing - It uses more memory
- It only matters in loops
Answer
2 — the implicit `None` looks like a legitimate value to the caller, so the mistake travels further into the program before anyone sees it. - What should the default case usually do when the input is something the program never expected?
- Pick the most common option and continue
- Do nothing
- Report the unexpected value clearly, by raising or printing a message
- Restart the program
Answer
3 — an unexpected value means something is wrong. Saying so, with the value included, is how the problem gets fixed. - When is
items[name]better thanitems.get(name, 0)?- Never
- When a missing name is a bug that should stop the program, and the docstring says it raises
KeyError - When
namecame from the user - When the dictionary is large
Answer
2 — raising is the right default when a missing key means something has gone wrong. The point is to choose and document the behavior, not to always pick one form.
Next up
Many of the problems in this lab can be found by a tool before you run the program. The last principle: heed static analysis warnings.