When match-case landed in Python 3.10, half the internet said "finally, Python has switch." That framing undersells it badly. If you use match as a switch, you'll conclude it's pointless — if/elif did that already.
What match actually does: it checks the shape of your data and destructures it in the same step. That's a different tool, and for certain code — API payloads, command parsers, state machines — it's a dramatically better one.
The switch part (30 seconds)
Yes, it does the boring thing:
match command:
case "quit":
save_and_exit()
case "help":
show_help()
case _: # wildcard: matches anything
print(f"Unknown command: {command}")
Fine. if/elif is just as good here. Moving on.
The real feature: matching structure
Say you're handling webhook events. The if version:
def handle(event):
if isinstance(event, dict) and event.get("type") == "user.created":
payload = event.get("payload")
if payload is not None:
create_user(payload)
elif isinstance(event, dict) and event.get("type") == "invoice.failed":
...
The match version says the same thing the way you'd describe it out loud:
def handle(event):
match event:
case {"type": "user.created", "payload": payload}:
create_user(payload)
case {"type": "invoice.failed", "invoice_id": inv_id}:
alert_finance(inv_id)
case {"type": str(other_type)}:
log.warning("unhandled event type %s", other_type)
case _:
raise ValueError(f"malformed event: {event!r}")
Each case simultaneously:
- checks the value is a dict,
- checks the keys and any literal values,
-
binds the parts you care about to names (
payload,inv_id).
The type check, the key check, and the unpacking collapse into one readable line. Extra keys in the dict are ignored — exactly what you want for API payloads that grow fields over time.
Sequences, classes, and guards
Sequences destructure like unpacking, with literals mixed in:
match args:
case []:
show_usage()
case ["--version"]:
print(VERSION)
case ["run", script, *rest]:
run(script, extra_args=rest)
Class patterns match attributes — clean over dataclasses:
@dataclass
class Click:
x: int
y: int
button: str = "left"
match event:
case Click(x=0, y=0):
print("origin clicked")
case Click(x=x, y=y, button="right"):
open_context_menu(x, y)
case KeyPress(key="q"):
quit()
Guards bolt a boolean condition onto a structural match:
match order:
case {"items": items, "total": total} if total > 10_000:
require_manual_review(order)
case {"items": []}:
raise EmptyOrderError
Alternatives with | share one body:
case "y" | "yes" | "ok":
proceed()
The classic gotcha
A bare name in a pattern is a capture, not a comparison:
RED = "red"
match color:
case RED: # does NOT compare against "red" —
stop() # it matches ANYTHING and rebinds RED to it
case "blue": # Python even raises SyntaxError here:
go() # "name capture 'RED' makes remaining patterns unreachable"
To compare against a constant, use a dotted name — that's the rule the language chose:
class Color:
RED = "red"
BLUE = "blue"
match color:
case Color.RED: # dotted = lookup and compare ✓
stop()
case Color.BLUE:
go()
Ruff and recent Python versions warn about suspicious always-true capture patterns, but knowing the rule beats trusting the linter.
When to still use if/elif
- Comparing one value against a few literals with no destructuring —
ifis simpler and works on every Python version. - Conditions on different variables per branch (
if x > 3 ... elif name == "bob") — that's not whatmatchis for. - Your code must run on Python < 3.10.
The sweet spot for match is exactly: "the shape of this data determines what happens next." Parsers, event handlers, recursive tree walks, REPL commands. In that territory it deletes entire pyramids of isinstance/.get() boilerplate.
Top comments (0)