match/case — structural pattern matching (3.10+)
PEP 634/636. More than just a switch statement: it pattern-matches structure.
Basic syntax
def http_status(code):
match code:
case 200 | 201 | 204:
return "ok"
case 301 | 302:
return "redirect"
case 400 | 404:
return "client error"
case 500 | 502 | 503:
return "server error"
case _:
return "unknown"
_ is the wildcard (matches anything, doesn’t bind). Use | for alternatives.
Capture patterns
match point:
case (0, 0):
print("origin")
case (x, 0):
print(f"on x-axis at {x}")
case (0, y):
print(f"on y-axis at {y}")
case (x, y):
print(f"({x}, {y})")
A bare name (x, y) captures the value into that name. Constants like 0 test equality.
Important: the capture binds in the enclosing scope. After the match, x and y are still defined.
Class patterns
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
@dataclass
class Circle:
center: Point
radius: float
def describe(shape):
match shape:
case Point(x=0, y=0):
return "origin"
case Point(x=x, y=y): # capture by attribute name
return f"point ({x}, {y})"
case Circle(center=Point(x=0, y=0), radius=r):
return f"circle at origin, radius {r}"
case Circle(center=c, radius=r):
return f"circle at {c}, radius {r}"
For positional matching, classes need __match_args__:
@dataclass
class Point:
x: int
y: int
__match_args__ = ("x", "y") # @dataclass auto-generates this
match p:
case Point(0, 0): ...
case Point(x, y): ...
@dataclass auto-generates __match_args__ for you.
Mapping patterns
def handle(event):
match event:
case {"type": "click", "x": x, "y": y}:
click(x, y)
case {"type": "key", "key": k}:
press(k)
case {"type": t, **rest}:
print(f"unknown {t}: {rest}")
Mapping patterns match dicts by required keys. Extra keys are ignored unless **rest captures them.
Sequence patterns
match data:
case []:
print("empty")
case [x]:
print(f"one element: {x}")
case [x, y]:
print(f"two: {x}, {y}")
case [first, *rest]:
print(f"head {first}, tail {rest}")
case [first, *_, last]:
print(f"first {first}, last {last}")
Works on any sequence (list, tuple), not just lists. Strings are iterable but excluded by design — case [x, y] won’t match "ab".
Guard clauses
match point:
case Point(x, y) if x == y:
print("on diagonal")
case Point(x, y) if x > 0 and y > 0:
print("first quadrant")
case Point(x, y):
print(f"({x}, {y})")
The if clause runs after the structural match — must evaluate to truthy for the case to be selected.
Literal vs name traps
case 0: # matches int 0
case None: # matches None
case True: # matches True
case Point(0): # matches Point with x=0 (positional)
case x: # captures any value into `x`! Not "the variable named x"
To compare against a variable, use a dotted reference:
THRESHOLD = 100
match value:
case THRESHOLD: # captures into THRESHOLD!
case .THRESHOLD: # SyntaxError
# Correct: use a class attribute or ENUM
class Config:
THRESHOLD = 100
match value:
case Config.THRESHOLD: # compares equality
...
This is a real footgun. Use UPPER constants on a class/module reference, not bare locals.
Real-world example: AST traversal
def evaluate(node):
match node:
case {"op": "+", "left": l, "right": r}:
return evaluate(l) + evaluate(r)
case {"op": "*", "left": l, "right": r}:
return evaluate(l) * evaluate(r)
case {"op": "lit", "value": v}:
return v
case _:
raise ValueError(f"unknown node: {node}")
Pattern matching shines for AST/event/protocol parsing.
Interview angle
- “What does
match/casegive you overif/elif?” (Structural deconstruction, exhaustiveness, capture-and-bind, more readable for nested data.) - “When does
case x:capture vs compare?” (Always captures ifxis a bare name. Use dotted access for constants.) - “Match a list with at least 2 elements” →
case [_, _, *_]:.