This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Cover All Cases

Every branch needs a path for the input you did not expect. Use defaults to handle it.

    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/elif chain over a menu choice, a status, or a command with no else.
    • A match statement with no wildcard case.
    • items[name] where name came from the user, with no check and no .get().
    • A function that returns a value inside an if and 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

    1. Finish every chain. Add else to every if/elif chain that selects among values, and case _: to every match.
    2. Make the default loud. The default should raise or print a clear message. It should not quietly skip or guess.
    3. 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.
    4. Write the valid set down. A set like KNOWN_STATUSES makes the check one line and gives the default case something to compare against.
    5. 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, let KeyError raise and document it.

    Knowledge Check

    • An if/elif chain handles choices “1”, “2”, and “3” with no else. The user enters “9”. What happens?
      1. Python raises an error
      2. Nothing; no branch runs and execution continues after the chain
      3. The first branch runs by default
      4. The program exits
      Answer2 — 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?
      1. Python forbids it
      2. It returns None, which the caller may treat as False, 0, or an empty value without noticing
      3. It uses more memory
      4. It only matters in loops
      Answer2 — 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?
      1. Pick the most common option and continue
      2. Do nothing
      3. Report the unexpected value clearly, by raising or printing a message
      4. Restart the program
      Answer3 — an unexpected value means something is wrong. Saying so, with the value included, is how the problem gets fixed.
    • When is items[name] better than items.get(name, 0)?
      1. Never
      2. When a missing name is a bug that should stop the program, and the docstring says it raises KeyError
      3. When name came from the user
      4. When the dictionary is large
      Answer2 — 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.