The match statement compares a value against a
series of patterns and runs the first one that fits. The Containers and Control
Flow chapter gave a first taste. This chapter covers the whole
feature, because match is far more than a
switch: a pattern can test a value’s shape, look inside
it, and pull out the parts you need, all in one step.
The simplest patterns are literal values. A case _
at the end is the wildcard: it matches anything, like a default.
Each case body runs only when its pattern matches, and
the first match wins:
# http_status.py
# Literal patterns match exact values; the _ wildcard is the default.
def describe(status: int) -> str:
match status:
case 200:
return "OK"
case 404:
return "Not Found"
case 500:
return "Server Error"
case _:
return f"status {status}"
print(describe(200))
print(describe(404))
print(describe(301))The output is:
OK
Not Found
status 301
For a plain value-to-value lookup like this, a dictionary is
often shorter (see the end of the chapter). match earns
its keep once the patterns do more than test equality.
Combine several patterns in one case with
|. A bare name is a capture pattern: it always
matches and binds the value to that name, which is the wildcard with
a name attached:
# move.py
# `|` lists alternatives in one case. A bare name captures the value.
def step(command: str) -> str:
match command:
case "up" | "u":
return "y -= 1"
case "down" | "d":
return "y += 1"
case other:
return f"unknown command: {other}"
print(step("up"))
print(step("d"))
print(step("jump"))The output is:
y -= 1
y += 1
unknown command: jump
A sequence pattern matches the shape of a list or tuple and binds
the elements by position. A starred name, as in *rest,
captures the remaining elements:
# sequence_patterns.py
# Sequence patterns match by length and position; *rest takes the
# remainder.
def summarize(items: list[int]) -> str:
match items:
case []:
return "empty"
case [only]:
return f"one item: {only}"
case [first, second]:
return f"two items: {first}, {second}"
case [first, *rest]:
return f"{first}, then {len(rest)} more"
case _:
return "unreachable"
print(summarize([]))
print(summarize([5]))
print(summarize([3, 4]))
print(summarize([1, 2, 3, 4]))The output is:
empty
one item: 5
two items: 3, 4
1, then 3 more
This is the structural part of “structural pattern matching”: the
pattern [first, second] matches only a two-element
sequence and pulls both out at once, with no indexing and no length
check.
A class pattern matches by type and extracts attributes. With a
data class you can match positionally, because
@dataclass fills in the __match_args__ the
pattern uses, or by keyword. The Data Classes as Types
chapter covers data classes:
# class_patterns.py
# A class pattern matches the type and binds attributes. A data class
# supports positional matching out of the box.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def locate(p: Point) -> str:
match p:
case Point(0, 0):
return "the origin"
case Point(0, y):
return f"on the y-axis at y={y}"
case Point(x, 0):
return f"on the x-axis at x={x}"
case Point(x, y):
return f"at ({x}, {y})"
print(locate(Point(0, 0)))
print(locate(Point(0, 5)))
print(locate(Point(3, 0)))
print(locate(Point(3, 4)))The output is:
the origin
on the y-axis at y=5
on the x-axis at x=3
at (3, 4)
Point(0, 0) matches a point whose fields are both
zero. Point(0, y) matches when x is zero
and captures y. The literal and the capture
combine in one pattern, which is the move that makes
match worth reaching for.
A guard is an if attached to a case.
The case matches only when the pattern fits and the guard
is true:
# guards.py
# A guard adds a condition to a case.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def quadrant(p: Point) -> str:
match p:
case Point(0, 0):
return "origin"
case Point(x, y) if x > 0 and y > 0:
return "first quadrant"
case Point(x, y) if x < 0 and y > 0:
return "second quadrant"
case _:
return "somewhere else"
print(quadrant(Point(0, 0)))
print(quadrant(Point(3, 4)))
print(quadrant(Point(-3, 4)))
print(quadrant(Point(-1, -1)))The output is:
origin
first quadrant
second quadrant
somewhere else
A mapping pattern matches keys in a dictionary and binds their values. Keys you do not mention are ignored, which makes it a clean way to dispatch on JSON-shaped data:
# mapping_patterns.py
# A mapping pattern matches the keys it names and binds their values.
def handle(event: dict[str, object]) -> str:
match event:
case {"type": "click", "x": x, "y": y}:
return f"click at ({x}, {y})"
case {"type": "key", "key": key}:
return f"key {key}"
case {"type": kind}:
return f"other event: {kind}"
case _:
return "not an event"
print(handle({"type": "click", "x": 10, "y": 20}))
print(handle({"type": "key", "key": "Enter"}))
print(handle({"type": "scroll", "delta": 3}))
print(handle({"button": 1}))The output is:
click at (10, 20)
key Enter
other event: scroll
not an event
When a value is one of a fixed set of types, declare that set as
a union with the type statement and match on it. End
with case _: assert_never(value). The type checker then
proves the match is exhaustive: if you add a type to the
union and forget its case, it reports the gap before the program
runs. This is the static-typing payoff from the Static Type Checking chapter
applied to control flow:
# exhaustive.py
# A closed union plus assert_never makes match exhaustive: forgetting
# a case becomes a type error, not a silent fall-through.
from dataclasses import dataclass
from math import pi
from typing import assert_never
@dataclass(frozen=True)
class Circle:
radius: float
@dataclass(frozen=True)
class Square:
side: float
type Shape = Circle | Square
def area(shape: Shape) -> float:
match shape:
case Circle(radius):
return pi * radius ** 2
case Square(side):
return side ** 2
case _:
assert_never(shape)
print(round(area(Circle(1.0)), 4))
print(area(Square(2.0)))The output is:
3.1416
4.0
Add a Triangle to Shape and the checker
flags assert_never(shape), because shape
could now be a Triangle that no case
handles. A switch cannot do this; neither can a chain
of if/isinstance. The Rethinking Objects chapter
uses exactly this technique to add operations to a closed set of
types without inheritance.
match is not a replacement for everything.
For a plain value-to-value lookup, a dictionary is shorter and faster:
# not_match.py
# A dict is the right tool for a value-to-value lookup; reserve match
# for patterns that destructure.
STATUS = {200: "OK", 404: "Not Found", 500: "Server Error"}
def describe(status: int) -> str:
return STATUS.get(status, f"status {status}")
print(describe(200))
print(describe(301))The output is:
OK
status 301
And when the set of types is open (anyone can add a new
one), polymorphism is better than a match: each type
carries its own behavior, so adding a type needs no change to a
central match. Use match when the set of
cases is closed and you want to handle them in one place, especially
when the cases need to look inside the value.
Each matcher is an ordinary function, so a test calls it and checks the result. The cases worth pinning down are the structural ones and the fall-through:
# test_pattern_matching.py
from class_patterns import Point, locate
from exhaustive import Circle, Square, area
from mapping_patterns import handle
from sequence_patterns import summarize
def test_sequence_patterns() -> None:
assert summarize([]) == "empty"
assert summarize([5]) == "one item: 5"
assert summarize([1, 2, 3]) == "1, then 2 more"
def test_class_patterns() -> None:
assert locate(Point(0, 0)) == "the origin"
assert locate(Point(3, 0)) == "on the x-axis at x=3"
assert locate(Point(3, 4)) == "at (3, 4)"
def test_mapping_patterns() -> None:
assert handle({"type": "key", "key": "Esc"}) == "key Esc"
assert handle({"nope": 1}) == "not an event"
def test_exhaustive_area() -> None:
assert round(area(Circle(1.0)), 4) == 3.1416
assert area(Square(2.0)) == 4.0classify(value) that uses match
to return "empty list", "singleton", or
"longer list" for lists, "point" for a
Point, and "other" for anything else.Rectangle type to
exhaustive.py’s Shape union
without adding its case. Run ty
and read the error it reports at assert_never.mapping_patterns.handle to also accept a
nested shape, such as
{"type": "click", "at": {"x": x, "y": y}}, binding
x and y from the inner dictionary.