Python Pattern Matching (match/case) Cheat Sheet
Structural pattern matching syntax introduced in Python 3.10: literal, capture, sequence, mapping, class patterns, and guards.
Basic match/case
Like switch, but matches structure, not just equality.
def http_status(code): match code: case 200: return "OK" case 404: return "Not Found" case 500 | 502 | 503: # OR pattern return "Server Error" case _: # wildcard, catches everything else return "Unknown"
Sequence & Capture Patterns
Destructure lists/tuples, bind names, and use * for the rest.
def describe(point): match point: case (0, 0): return "origin" case (0, y): return f"on y-axis at {y}" case (x, 0): return f"on x-axis at {x}" case (x, y): return f"point at ({x}, {y})" case [first, *rest]: return f"list starting with {first}, then {rest}" case _: return "not a point"
Mapping & Class Patterns
Match dict keys or destructure objects by attribute.
def handle(event): match event: case {"type": "click", "x": x, "y": y}: return f"click at {x},{y}" case {"type": "key", **rest}: # **rest captures remaining keys return f"key event: {rest}" case _: return "unhandled"from dataclasses import dataclass@dataclassclass Point: x: int y: intdef classify(p): match p: case Point(x=0, y=0): return "origin" case Point(x=0, y=y): return f"y-axis: {y}" case Point() as pt: # capture whole match with 'as' return f"somewhere: {pt}"
Guards & Nested Patterns
Add an if condition to a case, and nest patterns arbitrarily.
def categorize(value): match value: case int(n) if n < 0: return "negative int" case int(n) if n == 0: return "zero" case int(n): return "positive int" case [int(a), int(b)] if a == b: return "pair of equal ints" case str() as s if s.isupper(): return "shouty string" case _: return "other"
Pattern Types Reference
The building blocks that combine into a case pattern.
- literal pattern- case 42, case "foo", case None, case True
- capture pattern- case x binds the whole subject to name x
- wildcard pattern- case _ matches anything, binds nothing
- OR pattern- case 1 | 2 | 3 matches any of the alternatives
- sequence pattern- case [a, b, *rest] destructures list/tuple-like objects
- mapping pattern- case {"key": val} destructures dict-like objects
- class pattern- case ClassName(attr=val) matches type and attributes
- guard- case pattern if condition adds an extra boolean check
Positional Class Patterns via __match_args__
Class patterns can bind fields positionally if the class defines __match_args__ (auto-generated by @dataclass); keyword patterns always work regardless.
from dataclasses import dataclass@dataclassclass Point: x: int y: intprint(Point.__match_args__) # ('x', 'y') -- generated by @dataclass in field orderdef classify(p): match p: case Point(0, 0): # positional pattern, relies on __match_args__ order return "origin" case Point(x, y) if x == y: # mix positional capture with a guard return "on diagonal" case Point(x=x, y=0): # keyword patterns always work, no __match_args__ needed return f"x-axis at {x}" case _: return "elsewhere"class Manual: __match_args__ = ("a", "b") # any class can opt into positional matching manually def __init__(self, a, b): self.a, self.b = a, b
Dotted Value Patterns (Enums & Constants)
A dotted name in a pattern is a value pattern compared with ==, unlike a bare name which is always a capture.
from enum import Enum, autoclass Color(Enum): RED = auto() GREEN = auto() BLUE = auto()def name_for(c): match c: case Color.RED: # dotted name -> value pattern, compares equality return "red" case Color.GREEN | Color.BLUE: return "green or blue" case _: return "unknown"RED_ALIAS = Color.REDdef check(c): match c: case RED_ALIAS: # BUG: bare name is a CAPTURE, matches anything, return "always matches" # and shadows the module-level RED_ALIAS name
Nested Mapping/Sequence Destructuring
Mapping, sequence, and class patterns compose to destructure deeply nested JSON-like structures in a single case.
def handle(msg): match msg: case {"type": "batch", "items": [first, *rest]} if rest: return f"batch starting with {first}, {len(rest)} more" case {"type": "user", "profile": {"name": str(name), "roles": [*roles]}}: return f"user {name} with roles {roles}" case {"type": "error", "code": int(code)} if 400 <= code < 500: return f"client error {code}" case [{"id": int(i)}, {"id": int(j)}] if i != j: return "two distinct records" case _: return "unrecognized"
Type-Check-Only Patterns & Walrus Guards
case Type() with no arguments checks isinstance() without binding fields; guards can use := to compute and test in one step.
def describe(value): match value: case bool(): # must precede int() -- bool is a subclass of int return "boolean" case int() | float() as n if (doubled := n * 2) > 100: return f"large number, doubled = {doubled}" case str() | bytes(): return "text-like" case _ as other: return f"fallback, type is {type(other).__name__}"
match/case Gotchas Reference
Rules that trip up developers coming from switch statements in other languages.
- bool before int- case bool() must precede case int(), since bool is an int subclass and would otherwise be swallowed
- no fallthrough- the first matching case wins and execution stops, there is no break or implicit fall-through
- irrefutable pattern- a bare name or wildcard placed before the last case is dead code and triggers a SyntaxWarning
- class patterns need __match_args__- positional case ClassName(a, b) fails at runtime (TypeError) if the class has no __match_args__
- guards don't backtrack- if a pattern matches but its guard is False, matching moves to the NEXT case, never retries alternatives within the same pattern
- subject evaluated once- the match subject expression is evaluated exactly once, not re-evaluated per case
- dict patterns are partial- case {"a": 1} matches a dict with EXTRA keys too; use **rest or nothing to be explicit about what's captured
A bare name in a case pattern is always a capture (binds a new variable), never an equality check against an existing variable — to match against an existing variable's value use a dotted name or a guard, e.g. case x if x == existing_var, or case SomeEnum.VALUE for enum members.