Structural Pattern Matching (match)
match brings structural pattern matching to Python — a more powerful cousin of the switch statement found in other languages. It doesn't just compare a value against constants; it can destructure sequences, mappings, and objects, bind variables from their contents, and attach guard conditions, all in one readable construct.
Basic literal matching
At its simplest, match compares a subject value against a series of case patterns, running the body of the first one that matches and skipping the rest — there's no fallthrough like C's switch, so you don't need a break at the end of each case.
Matching literal values
def describe(value):
match value:
case 1:
return 'one'
case 2:
return 'two'
case 3:
return 'three'
case _:
return 'something else'
print(describe(2)) # two
print(describe(99)) # something elseOr-patterns with |
Use | to match several alternatives with a single case, avoiding repeated bodies for values that should be treated the same way.
Matching multiple values
def is_weekend(day):
match day:
case 'Saturday' | 'Sunday':
return True
case 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday':
return False
case _:
raise ValueError('not a valid day')
print(is_weekend('Sunday')) # True
print(is_weekend('Tuesday')) # FalseThe wildcard catch-all
case _: matches anything and binds nothing — it's the equivalent of a default case in a switch statement. Convention (and good practice) is to always include one unless you're intentionally certain every case is covered, since an unmatched subject with no wildcard simply falls through the whole match block without error and without running anything.
Guards: adding conditions to a case
A guard is an if clause attached to a case, letting you match a pattern only when an extra condition also holds. The pattern still has to match first; the guard is then evaluated, and if it's falsy, match moves on to try the next case as if this one had never matched at all.
Case guards
def classify(n):
match n:
case x if x < 0:
return 'negative'
case 0:
return 'zero'
case x if x % 2 == 0:
return 'positive even'
case _:
return 'positive odd'
print(classify(-5)) # negative
print(classify(0)) # zero
print(classify(4)) # positive even
print(classify(7)) # positive oddDestructuring lists and tuples
Sequence patterns can unpack a list or tuple by shape, bind names to specific positions, and use *rest to capture the remainder — very similar to unpacking assignment, but with the added power of matching by length and fixed elements at the same time. Patterns are tried top to bottom, so put the more specific shapes first.
Matching sequences
def describe_point(point):
match point:
case [0, 0]:
return 'origin'
case [x, 0]:
return f'on the x-axis at {x}'
case [0, y]:
return f'on the y-axis at {y}'
case [x, y]:
return f'point at ({x}, {y})'
case [x, *rest]:
return f'starts with {x}, then {len(rest)} more values'
case _:
return 'not a point'
print(describe_point([0, 0])) # origin
print(describe_point([3, 0])) # on the x-axis at 3
print(describe_point([2, 5])) # point at (2, 5)
print(describe_point([1, 2, 3, 4])) # starts with 1, then 3 more valuesDestructuring dictionaries
Mapping patterns match dictionaries by the keys you care about — extra keys in the dictionary are ignored automatically, so you don't need to check for exact shape unless you deliberately capture leftovers with **rest.
Matching dictionaries
def handle_event(event):
match event:
case {'type': 'click', 'x': x, 'y': y}:
return f'click at ({x}, {y})'
case {'type': 'keypress', 'key': key}:
return f'key pressed: {key}'
case {'type': event_type}:
return f'unhandled event type: {event_type}'
case _:
return 'not an event'
print(handle_event({'type': 'click', 'x': 10, 'y': 20}))
# click at (10, 20)
print(handle_event({'type': 'keypress', 'key': 'Enter'}))
# key pressed: EnterMatching classes with attribute patterns
Class patterns let you match on an object's type and its attributes simultaneously. This works great with dataclasses, since they generate readable __init__ and __repr__ methods for free, and their fields double as the attributes you match on.
Matching objects by attribute
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def describe(point):
match point:
case Point(x=0, y=0):
return 'origin'
case Point(x=0, y=y):
return f'on the y-axis at {y}'
case Point(x=x, y=0):
return f'on the x-axis at {x}'
case Point(x=x, y=y):
return f'point at ({x}, {y})'
case _:
return 'not a point'
print(describe(Point(0, 0))) # origin
print(describe(Point(3, 4))) # point at (3, 4)When is match more readable than if/elif?
match shines when you're branching on the shape of data — a value that could be one of several structured forms (like parsed JSON, an AST node, or a command with variable arguments) rather than a single scalar comparison. For simple linear conditions (age ranges, numeric thresholds), if/elif is usually just as clear and sometimes clearer. Reach for match when you find yourself writing nested isinstance checks, manual tuple unpacking with length checks, or repeated dict.get(...) calls just to figure out what kind of value you're looking at — match lets you express "if it looks like this, bind these names" in a single line per case.
matchrequires Python 3.10 or newer.case value:matches literals;case _:is the wildcard catch-all.case a | b:matches any of several alternatives.case x if condition:adds a guard clause to a pattern.case [x, y]:andcase [x, *rest]:destructure sequences.case {"key": value}:destructures mappings by key, ignoring extras.case ClassName(attr=value):matches an instance and destructures its attributes.