match Statement — Structural Pattern Matching (3.10+)
~20 min · match, pattern-matching, switch, 3.10
Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
Not a switch statement — something stranger and more useful
Python 3.10 added match, and on the surface it looks like a switch from C-family languages. It isn't. It's structural pattern matching — you're not just checking equality, you're describing the shape of the value (a list of three things, a dict with these keys, a class instance with these attributes), and Python destructures the match into named variables for you.
Cases — literal, capture, sequence, mapping, class
Five common patterns. Literal: case 0:, case "x":. Capture: case x: binds anything to x. Sequence: case [a, b, *rest]: destructures a list. Mapping: case {"name": name}: grabs a key. Class: case Point(x=0, y=y): matches a Point with x=0 and binds y. The _ wildcard is the catch-all default.
The OR pattern, the guard, and the AS clause
Patterns can be combined with |: case 1 | 2 | 3:. Guards (if after a pattern) refine when a case matches: case x if x > 0:. The as clause names a sub-pattern: case [Point(x, y) as p, *_]: binds the whole first element to p.
When NOT to use match
If your branches are simple equality checks — case 1: case 2: case 3: — a dict dispatch or an if-chain is just as clean. match earns its weight when you're destructuring a structured value (parsed AST, JSON-shaped data, custom classes). For the C-style switch use case, you didn't need match. For the "take this complex value apart" use case, match is the cleanest tool Python has.
Warning: A bare name in a case pattern is a capture, not a comparison. case foo: matches anything and binds it to foo. To compare against a constant variable, use a dotted name (case Status.ACTIVE:) or wrap it (case (CONST,):). This is the most common confusion.
Code
Basic match — literal and wildcard·python
def describe(status):
match status:
case 200:
return "OK"
case 301 | 302:
return "redirect"
case 404:
return "not found"
case n if 500 <= n < 600:
return "server error"
case _:
return "unknown"
print(describe(200)) # OK
print(describe(301)) # redirect
print(describe(503)) # server error
print(describe(700)) # unknown
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Circle:
center: Point
radius: float
def shape_summary(shape):
match shape:
case Point(0, 0):
return "origin"
case Point(x, 0):
return f"on x-axis at {x}"
case Point(x, y):
return f"point at ({x}, {y})"
case Circle(Point(0, 0), r):
return f"circle centered at origin, r={r}"
case Circle(_, r) if r > 100:
return f"big circle r={r}"
case _:
return "unknown shape"
print(shape_summary(Point(0, 0)))
print(shape_summary(Point(3, 0)))
print(shape_summary(Circle(Point(0, 0), 5)))
The capture vs constant gotcha·python
ACTIVE = "active"
INACTIVE = "inactive"
def status_of(s):
match s:
case ACTIVE: # WRONG — this is a capture, matches ANYTHING
return "is active"
case _:
return "unknown"
print(status_of("foo")) # 'is active' <- bug! captured 'foo' as ACTIVE
# RIGHT — use dotted name (e.g., enum or class attribute)
class Status:
ACTIVE = "active"
INACTIVE = "inactive"
def status_of_v2(s):
match s:
case Status.ACTIVE: # correct — comparison against constant
return "is active"
case _:
return "unknown"
print(status_of_v2("foo")) # 'unknown' ✓
print(status_of_v2("active")) # 'is active' ✓
Write a function process(message) that takes a dict and uses match to dispatch on its type field. Handle three cases: {"type": "chat", "text": ...} returns the text uppercased; {"type": "command", "name": ..., "args": [...]} returns a string showing the command + arg count; {"type": "event", "name": ..., **rest} returns the event name and the count of extra fields. Use a wildcard for unknown shapes that returns "unknown". Test with at least four messages.
Progress
Progress is local-only — sign in to sync across devices.