PythonStructural Pattern Matching (match)

Structural Pattern Matching (match)

Requires Python 3.10+
The `match` statement was introduced in Python 3.10 via PEP 634. If you're on an older Python version, you'll need to keep using `if`/`elif`/`else` chains instead — `match` will raise a `SyntaxError` on earlier interpreters.

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

Python
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 else
Or-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

Python
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'))  # False
The 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

Python
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 odd
Destructuring 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

Python
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 values
Destructuring 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

Python
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: Enter
Matching 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

Python
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.

  • match requires 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]: and case [x, *rest]: destructure sequences.

  • case {"key": value}: destructures mappings by key, ignoring extras.

  • case ClassName(attr=value): matches an instance and destructures its attributes.