Structural pattern matching, introduced by PEP 634 in Python 3.10, is a match statement that compares a subject value against a series of case patterns. Patterns can match literals, capture values, destructure sequences and mappings, and match class instances by attribute.
Smarter than a switch statement
What you’ll learn: The match statement (Python 3.10+) — matching a value against literal patterns, the wildcard _ case, combining patterns with |, and capturing/destructuring values.
How to read this tab: Requires Python 3.10+. Start with literal matching, then move to destructuring — that is where match beats if/elif.
A match statement looks at a value and checks it against several patterns, running the code for the first one that fits. It is like a smarter if/elif chain that can also look inside the data — matching not just values but their shape.
match requires Python 3.10 or newer. On older versions it is a syntax error. match and case are "soft keywords" — they only act as keywords inside a match statement, so existing variables named match or case still work elsewhere.
match basics
The simplest form of match compares a value against literal patterns — close to a switch statement in other languages, but with one crucial extra: the wildcard _ case.
def describe_status(code):
match code:
case 200:
return "OK"
case 404:
return "Not Found"
case 500:
return "Server Error"
case _: # wildcard — matches anything (the "default")
return "Unknown"
print(describe_status(200)) # OK
print(describe_status(999)) # Unknown
# Combine patterns with | (OR)
def category(code):
match code:
case 200 | 201 | 204:
return "Success"
case 400 | 401 | 403 | 404:
return "Client Error"
case 500 | 502 | 503:
return "Server Error"
case _:
return "Other"
print(category(404)) # Client ErrorStructural pattern matching was added in Python 3.10 (October 2021). On older versions, match is a syntax error. Check with python --version. match and case are "soft keywords" — they are only keywords inside a match statement, so existing code using them as variable names still works.
The real power is destructuring — matching the shape of data. case ["go", direction] matches a two-element list starting with "go" and captures the second element. This is what makes match fundamentally more capable than a switch on a single value.
Capturing and destructuring
The real power of match appears when patterns capture values and destructure sequences — pulling apart the structure of the data in the pattern itself.
def handle_command(command):
match command.split():
case ["quit"]:
return "Quitting"
case ["go", direction]: # capture the second word
return f"Moving {direction}"
case ["pick", "up", *items]: # capture the rest as a list
return f"Picking up: {items}"
case _:
return "Unknown command"
print(handle_command("go north")) # Moving north
print(handle_command("pick up sword shield")) # Picking up: ['sword', 'shield']
# Destructuring a coordinate tuple
def quadrant(point):
match point:
case (0, 0):
return "origin"
case (x, 0): # captures x, requires y == 0
return f"on x-axis at {x}"
case (0, y):
return f"on y-axis at {y}"
case (x, y):
return f"point at ({x}, {y})"
print(quadrant((0, 0))) # origin
print(quadrant((5, 0))) # on x-axis at 5
print(quadrant((3, 4))) # point at (3, 4)✅ Beginner tab complete
- I can write a match with literal cases and a _ wildcard default
- I can combine patterns with | (OR)
- I can destructure a sequence: case [x, y] or case ["go", direction]
- I know a bare name in a case captures, it does not compare
Class patterns, guards, and mappings
What you’ll learn: Matching class instances by attribute (Circle(radius=r)), adding if guards to patterns, matching dict structure (mapping patterns), and the as keyword for capture.
How to read this tab: Class patterns plus dataclasses are a powerful combination — they replace long isinstance chains with clean declarative code.
Class patterns + dataclasses replace isinstance chains. Instead of if isinstance(s, Circle): ... elif isinstance(s, Rectangle): ..., write case Circle(radius=r): and case Rectangle(width=w, height=h): — matching the type and extracting attributes together.
Class patterns
Patterns can match instances of a class and simultaneously extract their attributes — exceptionally useful for processing structured objects without a chain of isinstance checks.
from dataclasses import dataclass
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
@dataclass
class Triangle:
base: float
height: float
def area(shape):
match shape:
case Circle(radius=r):
return 3.14159 * r * r
case Rectangle(width=w, height=h):
return w * h
case Triangle(base=b, height=h):
return 0.5 * b * h
case _:
raise ValueError("Unknown shape")
print(area(Circle(5))) # 78.53...
print(area(Rectangle(4, 6))) # 24
print(area(Triangle(3, 8))) # 12.0
# Positional class patterns using __match_args__
@dataclass
class Point:
x: int
y: int
# dataclasses set __match_args__ = ('x', 'y') automatically
def origin_check(p):
match p:
case Point(0, 0): # positional — uses __match_args__
return "at origin"
case Point(x, y):
return f"at ({x}, {y})"
print(origin_check(Point(0, 0))) # at originA bare name captures — it never compares. case x: matches anything and binds it to x. To compare against a constant, use a dotted name (case Color.RED:) or a literal. case RED: creates a new variable RED rather than comparing to an existing one — a frequent and silent mistake.
Guards and mapping patterns
# Guards — an if condition attached to a pattern
def classify(point):
match point:
case (x, y) if x == y: # guard
return "on diagonal"
case (x, y) if x > 0 and y > 0:
return "first quadrant"
case (x, y):
return "elsewhere"
print(classify((3, 3))) # on diagonal
print(classify((2, 5))) # first quadrant
# Mapping patterns — match dict structure (e.g. JSON)
def handle_event(event):
match event:
case {"type": "click", "x": x, "y": y}:
return f"Click at ({x}, {y})"
case {"type": "key", "key": key}:
return f"Key pressed: {key}"
case {"type": event_type}:
return f"Unknown event: {event_type}"
case _:
return "Malformed event"
print(handle_event({"type": "click", "x": 10, "y": 20})) # Click at (10, 20)
print(handle_event({"type": "key", "key": "Enter"})) # Key pressed: Enter
# Capture with 'as'
def describe(obj):
match obj:
case [Point(0, 0) as p]:
return f"single origin point: {p}"
case [*points]:
return f"{len(points)} points"case x: matches anything and binds it to x. To compare against an existing constant, use a dotted name (case Color.RED:) or a literal. case RED: where RED is a variable will NOT compare to RED — it captures into a new variable named RED.✅ Intermediate tab complete
- I can match a class instance and extract its attributes in one pattern
- I can add a guard: case (x, y) if x == y
- I can match dict structure with mapping patterns
- I know mapping patterns match a SUBSET of keys (extra keys are ignored)
PEP 634 semantics and __match_args__
What you’ll learn: The formal matching semantics — irrefutable patterns, no fall-through, __match_args__ for positional class patterns, and why str/bytes are excluded from sequence patterns.
How to read this tab: Read when implementing complex pattern matching or designing classes meant to be matched positionally.
Match semantics, irrefutability, and __match_args__
PEP 634 defines the formal semantics. Patterns are tried top to bottom; the first matching case runs and no others (no fall-through, unlike C's switch). A pattern can be irrefutable — guaranteed to match — such as a bare capture pattern (case x:) or the wildcard (case _:). An irrefutable pattern must be last, because any case after it is unreachable; the compiler does not enforce this but the pattern can never trigger.
# __match_args__ controls positional class patterns
class Point:
__match_args__ = ("x", "y") # positional order for matching
def __init__(self, x, y):
self.x = x
self.y = y
# Now Point(0, 0) in a pattern checks x==0, y==0 positionally
match Point(1, 2):
case Point(1, y): # uses __match_args__ to map position 0 -> x
print(f"x is 1, y is {y}") # x is 1, y is 2
# Sequence patterns work on any Sequence EXCEPT str/bytes/bytearray
# (deliberately — so "abc" is not treated as ['a','b','c'])
match [1, 2, 3]:
case [first, *rest]:
print(first, rest) # 1 [2, 3]
# The compiler emits MATCH_SEQUENCE, MATCH_MAPPING, MATCH_CLASS opcodes
import dis
def f(x):
match x:
case [a, b]: return a + b
case _: return 0
dis.dis(f) # shows MATCH_SEQUENCE, GET_LEN, etc.Sequence patterns deliberately exclude str, bytes, and bytearray — matching "abc" against case [a, b, c] fails rather than destructuring into characters, because treating strings as character sequences in pattern matching is almost always a bug. Mapping patterns match a subset of keys: case {"type": t} matches any dict containing a "type" key, ignoring other keys — unlike sequence patterns which match the full length unless a star pattern is used.
✅ Expert tab complete
- I know patterns are tried top-to-bottom with no fall-through
- I know __match_args__ defines the positional order for class patterns
- I know str/bytes are deliberately excluded from sequence patterns