match-case statement
Overview
`match-case` (Python 3.10+) provides structural pattern matching. It compares a subject against the STRUCTURE of each `case`, not just its equality. A capture pattern binds a name directly from inside the matched structure: def route(event): match event: case {'type': 'login', 'user': user}: return f'login by {user}' case {'type': 'logout'}: return 'logout' case _: return 'unknown' The first `case` matches any `dict` containing both a `'type'` key equal to `'login'` and a `'user'` key. The value under `'user'` is captured into the variable `user`. The wildcard `_` is the catch-all — it matches anything and binds nothing. Like `if`/`elif`, only the first matching `case` runs. You can also OR literal values using the `|` operator, like `case 'GET' | 'HEAD':` to match either string. A major trap is that a bare variable name in a `case` always captures the subject; it never compares against an existing variable: STATUS_OK = 200 match response_code: case STATUS_OK: # TRAP! Captures response_code into new variable STATUS_OK pass case code if code == STATUS_OK: # CORRECT: Uses a guard pass To match against an existing variable's value, you must use a guard (`if`) or use a dotted name (like `http.HTTPStatus.OK`). Another sharp edge is that sequence patterns require an exact length match by default. If your list has extra items, the match will fail: event = ['drop', 'table', 'users', 'cascade'] match event: case ['drop', 'table']: pass # Fails: sequence length must match exactly case ['drop', 'table', *rest]: pass # Works: *rest captures remaining items To match a prefix and ignore or capture the rest, you must use a starred expression like `*rest`.
`match-case` replaces cascading `isinstance` checks and multi-key `dict` lookups when dispatching on message types or API events. Without it, routing a dozen event types requires a long `if`/`elif` chain full of repeated `dict.get()` calls.
Where used: JSON webhook event routing, CLI subcommand dispatch, protocol message type handlers
Why learn this
- Replacing cascading `isinstance` / `dict`-key `if`/`elif` chains with declarative structural dispatch on API payloads
- Reading modern Python 3.10+ code that uses pattern matching on JSON events and command `struct`s
Code walkthrough
def handle(event):
match event:
case {'type': 'signup', 'email': email}:
return f'new user: {email}'
case {'type': 'logout'}:
return 'logged out'
case _:
return 'unknown'
print(handle({'type': 'signup', 'email': 'a@b.com'}))
print(handle({'type': 'logout'}))
print(handle({'type': 'ping'}))
Focus: The first `case` matches the dict's structure AND binds the `'email'` value to a variable; the second matches on type alone; `_` catches anything unmatched.
Common mistakes
- Expecting a bare variable name to compare, not capture: `case status_code:` does NOT compare against an existing variable — it captures the subject into a new name `status_code`. To compare against a variable, use a guard: `case x if x == status_code:` or a dotted name.
- Forgetting the wildcard and getting no match: If no `case` matches and there is no `_` catch-all, `match` silently does nothing — there is no error. Always add a `case _:` branch for unexpected values.
- Using match for simple equality dispatch: `match` is most valuable for structural patterns (`dict`s, nested objects). For plain value equality on a string or `int`, a `dict` lookup or `if`/`elif` is simpler and more readable.
Glossary
- structural pattern matching
- Checking data against a specific layout or structure, rather than just comparing simple values. For example, `case {'type': 'login', 'user': u}:` matches any dict with those two keys.
- wildcard
- A special symbol (like `_`) that acts as a catch-all, matching anything that hasn't been specifically caught by other rules. For example, `case _: return 'unknown'`.
Recall questions
- What does the wildcard `_` do in a `match`-`case` statement?
- What happens when a capture pattern like `user` appears inside a dict pattern?
- What happens if no `case` branch matches and there is no wildcard?
- How do you match either of two literal values in a single `case`?
- How do you compare a subject against the value of an existing variable in a `match` statement?
- How do you compare a subject against the value of an existing variable in a `match` statement?
- How do you match a sequence pattern against a list that has an unknown number of extra items at the end?
Understanding checks
What does this print, and why?
`A`
Two rules combine: the first matching `case` wins, AND a mapping pattern matches any `dict` that CONTAINS the given keys (extra keys are allowed). So `{'type': 'login'}` already matches the event, and the broader `case` fires first — the more specific `case` is never reached. Most people guess `B`.
What does this print?
`done`
No `case` matches `500` and there is no wildcard `case _`, so the `match` statement simply does nothing — it is not an error. Execution continues to the `print` after the `match`.
Practice tasks
HTTP method dispatcher
Modify the given `dispatch(method)` function. Replace the `pass` statement with a `match`-`case` block that returns `'read'` for `'GET'`, `'write'` for `'POST'` or `'PUT'`, `'delete'` for `'DELETE'`, and `'unsupported'` for anything else.
Challenge
API event router
Write `route_event(event)` that handles `dict` payloads. If `type='db_query'` and a `'table'` key is present, return `'querying <table>'`. If `type='cache_clear'`, return `'clearing cache'`. If `type='healthcheck'`, return `'ok'`. For any other type, return `'unhandled: <type>'`. Test all four patterns.
Continue learning
Previous: if/elif/else and Python truthiness
Previous: Dictionaries: operations and use cases