Pattern matching
Python 3.10 introduced structural pattern matching — the match/case statement (PEP 634, 635, 636). The mechanism admits dispatching on both the value and the structure of an expression: literal matches, sequence patterns, mapping patterns, class patterns, and combinations of these. The form fills a gap between if/elif chains (good for value comparison but verbose for structural checks) and the runtime polymorphism of method dispatch (good for type-driven code but heavy when the structure is the principal axis of variation). Modern Python uses match extensively for sum-type discrimination, parsing, and protocol implementation.
This page covers the pattern grammar, the principal pattern forms, exhaustiveness, and the conventions for using match in idiomatic code.
The match statement
The basic form:
match subject:
case pattern1:
body1
case pattern2 if guard:
body2
case _:
default_body
The subject is evaluated; the patterns are tried in order; the first matching pattern’s body executes. The _ is the wildcard — matches anything; conventional for the catch-all default.
def classify(n):
match n:
case 0:
return "zero"
case 1:
return "one"
case 2 | 3 | 5 | 7 | 11 | 13:
return "prime"
case _:
return "other"
The | admits or-patterns — match any of the listed alternatives.
Pattern forms
The principal patterns:
| Pattern | Example | Matches |
|---|---|---|
| Literal | 42, "x", True, None | The exact value |
| Capture | n, name | Anything; binds to the variable |
| Wildcard | _ | Anything; binds nothing |
| Or | 1 | 2 | 3 | Any of the alternatives |
| Sequence | [1, 2, x], (a, b), [] | Sequences of matching shape |
| Mapping | {"key": x}, {} | Mappings with the named keys |
| Class | Point(x=0, y=0) | Instances of the class with matching fields |
| As | [1, 2, 3] as triple | Match and bind the whole |
| Guard | case n if n > 0: | Match plus a boolean condition |
Literal patterns
Match against constant values:
match command:
case "quit":
exit()
case "help":
show_help()
case "":
prompt()
The literal patterns admit numbers, strings, True, False, None, and qualified attributes like Color.RED:
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
match c:
case Color.RED:
return "red"
case Color.GREEN:
return "green"
case Color.BLUE:
return "blue"
The qualified-name form (Color.RED) is the conventional way to match against an enum value.
Capture patterns
A simple identifier captures the matched value:
match value:
case n: # captures anything; binds to n
print(f"got {n}")
A capture pattern alone matches anything — equivalent to case _: plus a binding. It is conventionally used inside more elaborate patterns:
match point:
case (x, y): # tuple destructuring; x and y are captures
return f"({x}, {y})"
The _ wildcard does not bind; capture patterns do. The distinction matters when the value is needed in the body.
Or-patterns
Match if any alternative matches:
match status:
case "ok" | "done" | "complete":
return Success()
case "fail" | "error" | "broken":
return Failure()
Captures inside or-patterns must bind the same names across alternatives:
match request:
case Move(x, y) | Resize(x, y): # both bind x and y
process_coordinates(x, y)
Sequence patterns
Match against sequences:
match items:
case []:
return "empty"
case [x]:
return f"single: {x}"
case [x, y]:
return f"pair: {x}, {y}"
case [first, *rest]:
return f"first: {first}, rest: {rest}"
case [*head, last]:
return f"head: {head}, last: {last}"
case [first, *middle, last]:
return f"first: {first}, middle: {middle}, last: {last}"
The *name admits matching a variable-length subsequence; *_ admits an unnamed subsequence. The pattern works on any sequence — list, tuple, string-like (with care; strings match characters), range.
Mapping patterns
Match against dictionaries:
match data:
case {}:
return "empty"
case {"name": n, "age": a}:
return f"{n} is {a}"
case {"error": msg}:
return f"error: {msg}"
case {"type": "click", "x": x, "y": y}:
handle_click(x, y)
The match succeeds if the dict has at least the named keys; extra keys are admitted. To capture remaining items:
match data:
case {"name": n, **rest}:
return f"{n} with extra: {rest}"
The **rest admits matching the remaining keys.
The mapping pattern is the conventional Python form for “JSON-like dispatch”.
Class patterns
Match against class instances:
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
match shape:
case Point(x=0, y=0):
return "origin"
case Point(x=0, y=y):
return f"on y-axis at {y}"
case Point(x=x, y=0):
return f"on x-axis at {x}"
case Point(x=x, y=y):
return f"({x}, {y})"
The form Point(x=0, y=0) matches if the value is a Point and its x is 0 and its y is 0. The keyword form (x=0) matches against the named attribute.
For positional matching, the class declares __match_args__:
class Point:
__match_args__ = ("x", "y")
def __init__(self, x: float, y: float):
self.x = x
self.y = y
match shape:
case Point(0, 0): # positional; uses __match_args__
return "origin"
case Point(x, y):
return f"({x}, {y})"
For dataclasses, __match_args__ is set automatically:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
match shape:
case Point(0, 0):
return "origin"
case Point(x=0, y=y): # keyword form also works
return f"y-axis at {y}"
case Point(x, y): # positional
return f"({x}, {y})"
The conventional contemporary approach is to use dataclasses; the positional and keyword forms both work without manual __match_args__.
Built-in matches
Several built-ins admit single-positional matching:
match value:
case bool(True):
return "true"
case int(n):
return f"int: {n}"
case str(s):
return f"string: {s}"
case list(xs):
return f"list: {xs}"
For built-in types, a single-positional class pattern matches the value and captures it under the given name. The form is the conventional Python idiom for type-and-bind in match.
Guards
A pattern may carry a when-style condition (Python uses if for guards):
match value:
case n if n > 0:
return "positive"
case n if n < 0:
return "negative"
case 0:
return "zero"
The guard is evaluated after the pattern matches; if false, the case is rejected and the next is tried.
Guards admit arbitrary boolean expressions:
match (status, value):
case ("ok", v) if v > 0:
return "positive ok"
case ("ok", _):
return "non-positive ok"
case _:
return "not ok"
As-patterns
Capture the whole value in addition to the destructured parts:
match point:
case [x, y] as p:
return f"point {p} with x={x}"
The as admits the whole sequence-pattern’s value to bind to p. The conventional uses are when both the structure and the value are needed.
Recursive patterns
Patterns nest:
match tree:
case Node(value=v, left=Node(), right=Node()) as full:
# both children are also Nodes
return f"full node {v}"
case Node(value=v, left=None, right=None):
# leaf
return f"leaf {v}"
case Node(value=v):
return f"partial node {v}"
The pattern admits substantial structural matching — the closest Python comes to algebraic-data-type matching of functional languages.
Common patterns
Tagged dispatch
def handle_event(event: dict) -> None:
match event:
case {"type": "click", "x": x, "y": y}:
on_click(x, y)
case {"type": "key", "code": code}:
on_key(code)
case {"type": "scroll", "delta": delta}:
on_scroll(delta)
case {"type": kind, **_}:
log_unknown(kind, event)
The conventional Python form for “JSON-like discrimination”. match is substantially clearer than the equivalent if/elif cascade.
Sum-type via dataclass
from dataclasses import dataclass
@dataclass
class Click:
x: int
y: int
@dataclass
class Key:
code: int
@dataclass
class Scroll:
delta: int
Event = Click | Key | Scroll # type alias
def handle(event: Event) -> None:
match event:
case Click(x, y):
on_click(x, y)
case Key(code):
on_key(code)
case Scroll(delta):
on_scroll(delta)
The combination of dataclass + Union + match is the conventional Python idiom for algebraic data types.
Result type
from dataclasses import dataclass
@dataclass
class Success[T]:
value: T
@dataclass
class Failure:
error: str
type Result[T] = Success[T] | Failure
def compute() -> Result[int]:
if condition:
return Success(42)
return Failure("oops")
def display(r: Result[int]) -> str:
match r:
case Success(v):
return f"value: {v}"
case Failure(e):
return f"error: {e}"
The pattern admits typed value-or-error returns.
Tuple destructuring
match point:
case (0, 0):
return "origin"
case (x, 0):
return f"x={x}"
case (0, y):
return f"y={y}"
case (x, y):
return f"({x}, {y})"
Tuple destructuring admits dispatch on the shape and contents of a coordinate-like value.
Sequence head and tail
match items:
case []:
return "empty"
case [head, *tail]:
return f"head={head}, tail={tail}"
The head-and-tail pattern admits recursive list processing similar to Haskell.
Match against literals with capture
match command_args:
case ("--verbose",):
verbose = True
case ("-o", path):
output = path
case ("-n", n):
count = int(n)
case args:
log(f"unrecognised args: {args}")
The pattern admits parsing simple command-line arguments.
When match is not the right tool
The conventional contemporary advice:
- Use
matchfor structural discrimination — when the shape of the value matters. - Use
if/eliffor boolean discrimination — when the conditions are arbitrary expressions. - Use polymorphism (subclasses with their own methods) when the operation varies by type and the type set is open.
# Boolean — use if/elif:
if x > 100:
handle_large(x)
elif x > 10:
handle_medium(x)
else:
handle_small(x)
# Structural — use match:
match value:
case [a, b, *_]:
...
case {"type": t, **kwargs}:
...
# Polymorphism — use methods:
shape.area() # dispatches on the type of shape
The choice depends on the axis of variation; match is the right tool when the structure is the principal axis.
A note on the limits of match
Python’s match is less elaborate than functional-language pattern matching:
- No exhaustiveness checking — the type checker (mypy, pyright) attempts it but is not always complete.
- No view patterns — patterns cannot involve arbitrary function calls.
- Capture is unguarded — a bare name (
x) captures rather than matches a constant; named constants must be qualified (Color.RED). - No relational patterns — there is no
case > 0:; use guards instead. - Limited or-pattern composition — captures must align across alternatives.
The combination is nonetheless substantial; modern Python uses match for many of the cases that older code wrote as long if/elif chains or explicit visitor patterns. The discipline of using match where it shines and if elsewhere is part of fluency in contemporary Python.