skip to content

In Python, what happens when a match statement's subject matches no case?

level: middleimportance: must knowfreq 45%

answer

  1. Compare it to an if/elif with no else
  2. The statement produces no value at all
  3. The grammar offers no else clause here
  4. Failure surfaces later, not at the match
  5. Silent fall-through unless you add one case

basics

~20 s

Nothing happens. The whole match statement becomes a no-op: no exception is raised, no default branch runs, and execution continues at the next statement. To make an unmatched subject an error, add case _: and raise there yourself.

solid answer

~50 s

A `match` whose cases all fail simply does nothing and falls through — there is no `MatchError`, no warning, and the grammar has no `else` clause to catch it. It behaves exactly like an `if`/`elif` chain with no `else`, which is the analogy PEP 634 itself uses, and it is deliberate: `match` is a statement, not an expression, so there is no result it is obliged to produce. The danger is that the failure is invisible at the point where it happens and only surfaces later, as a value that never got updated, a record that never got written, or an `UnboundLocalError` from a name that every case would have assigned. When the subject comes from a set you believe is closed, close the statement yourself with `case _:` and raise, interpolating the unmatched subject into the message.

code

python · 9 lines
python
def apply_credit(code, cents):
    match code:
        case "WELCOME":
            cents -= 1000
        case "LOYALTY":
            cents -= 2500
    return cents

print(apply_credit("WELCOME", 9200), apply_credit("EXPIRED", 9200))

go deeper

for a junior

Know the headline: nothing happens, and execution just continues. Being able to say the match is simply skipped, with no error, already answers the question.

for a middle

Explain the mechanics and the rationale — a statement rather than an expression, modelled on if/elif with no else, no else clause in the grammar, case _: as the only default.

for a senior

Show that you have debugged the consequences: a stale value that ships wrong numbers, an UnboundLocalError far from the cause, or a batch that quietly processes fewer items than it read.

for a principal

Frame the choice between raising, logging and passing as a policy question tied to who owns the input domain, and make sure the codebase applies it consistently rather than one match at a time.

The answer is almost aggressively boring: **nothing happens**. Python evaluates the subject, tries each pattern in order, finds that none matches, and moves on to the statement after the `match` block. No exception. No warning. No default. Interviewers ask it because candidates arriving from languages with exhaustive `switch` or `match` constructs expect a compile error or a runtime error, and Python offers neither. ### Why the language chose that PEP 634 frames `match` as a generalisation of the `if`/`elif` chain, and an `if`/`elif` chain with no `else` is likewise a no-op when every test is false. Nobody expects that to raise, and `match` inherits the expectation. Reinforcing it, `match` is a **statement**, not an expression: it produces no value, so there is no hole that a default branch would have to fill. In a language where `match` were an expression, an unmatched subject would leave the expression with nothing to evaluate to, and an error would be forced. Python sidestepped that by never making the construct produce a value in the first place. The grammar detail worth knowing: the statement has **no `else:` clause**. `for` and `while` have one, `try` has one, `match` does not. The wildcard case, `case _:`, is the only way to write a default branch. ### How the silence bites Because the no-op is invisible where it happens, the symptom always appears somewhere else. Three shapes recur. **A stale value.** The variable the cases were supposed to update simply keeps the value it had before the match. Downstream code sees a plausible number and never suspects it is the previous iteration's. **An unbound name.** If every case assigns a name and nothing else does, a non-matching subject leaves it unbound, and the first read raises `UnboundLocalError: cannot access local variable 'amount' where it is not associated with a value`. That is the *lucky* case — it is loud, immediate, and points at the right function. **Silently dropped work.** The most expensive shape. A dispatch loop matches each incoming item and performs a side effect in each case; an item of an unrecognised kind produces no side effect and no complaint, so the loop reports success over a smaller set of results than it received. ```python def credit_cents(code): match code: case "WELCOME": amount = 1000 case "LOYALTY": amount = 2500 return amount credit_cents("EXPIRED") # UnboundLocalError ``` ### Making it loud The fix is one case, and the choice of what goes in it is a real design decision rather than a formality. *Raise*, when the subject is drawn from a set your own code owns — an enum, a set of message kinds, a state machine's states. A `case _:` that raises `ValueError` or `NotImplementedError` with the subject in the message converts a silent skip into a stack trace that names the value and the line. Interpolate the value with `!r` and include its type, because "unhandled plan: 'pro'" versus "unhandled plan: Plan.PRO" is usually the whole diagnosis: a string arrived where an enum member was expected. *Log and continue*, when the subject comes from outside — a third-party feed, a file format that gains new record kinds, user input. Here an unknown kind is expected in the ordinary course of business and crashing the whole run over one record is the wrong trade. Log at warning level with enough of the subject to identify it, and count it, so an unexpected surge is visible on a dashboard rather than buried in a log file. *Pass, explicitly.* Sometimes falling through really is correct — a filter that only cares about two of many kinds. Write `case _: pass` anyway. It costs one line and tells the next reader that the fall-through was decided rather than forgotten, which is exactly the information an empty statement cannot convey on its own. ### The check that never runs One last point worth pre-empting: the interpreter never inspects a match statement to see whether the cases could collectively cover the subject. Cases are tried, not analysed. So the silence is not an oversight in a particular match — it is the language's uniform behaviour, and closing the gap is always the author's job.

  • Does a match statement have an `else:` clause, the way a `for` loop does?
    No. The grammar has no `else` for `match`; `case _:` is the only default branch. That is worth stating explicitly in an interview, because candidates often assume symmetry with `for`/`while`/`try` and then write an `else` that is a plain `SyntaxError`.
  • If every case assigns a result and nothing matches, what does the code that reads it see?
    Whatever was there before. Inside a function, if the name was never bound, the read raises `UnboundLocalError`; if it was bound earlier — a loop variable from a previous iteration, say — it silently keeps the stale value, which is far worse because the run completes and the number looks credible.
  • When is a raising catch-all the wrong choice?
    When the subject comes from outside your control and new kinds are expected: a partner feed, a versioned file format, user input. There, one unknown record should not abort the whole run. Log it with enough detail to identify it, count it so a surge is visible, and keep processing — but never leave the case out entirely.

saying these in an interview costs you the question

  • Expects a MatchError or similar when nothing matches
  • Thinks `match` supports an `else:` clause
  • Assumes the final case runs as an implicit default
  • Believes the interpreter warns about an unhandled subject
  • Says an unmatched subject aborts the enclosing function
  • Leaves no catch-all over a set the code itself owns

context