Home Tutorials Core Syntax

Core Syntax

Structural Pattern Matching: Python's match Statement Explained

Pyford Notes July 6, 2026 7 min read
Key points
  • match/case, added in Python 3.10, compares a value against a series of patterns and runs the first one that fits.
  • It goes well beyond a simple switch statement: patterns can unpack sequences, match object attributes, and bind variables as they match.
  • A bare name in a case pattern always matches and binds — use case _: for a true catch-all that binds nothing.
  • if guard clauses on a case add extra conditions beyond what the pattern shape alone can express.

Basic syntax: match and case

A match statement takes a subject value and a series of case clauses, each specifying a pattern to compare against:

def describe_status(code):
    match code:
        case 200:
            return "OK"
        case 301 | 302:
            return "Redirect"
        case 404:
            return "Not found"
        case 500:
            return "Server error"
        case _:
            return "Unknown status"

print(describe_status(404))   # Not found
print(describe_status(301))   # Redirect

The _ pattern is a wildcard that matches anything and is conventionally placed last, functioning like an else branch. Python checks cases top to bottom and stops at the first match, the same order-dependence as a chain of if/elif statements.

More than a switch statement

Languages with a switch statement typically only compare a value for equality against a list of constants. Python's match does that, but also supports structural patterns that inspect the shape of a value, not just its equality:

def handle(command):
    match command.split():
        case ["go", direction]:
            return f"Moving {direction}"
        case ["take", item]:
            return f"Picking up {item}"
        case ["look"]:
            return "Looking around"
        case _:
            return "Unknown command"

print(handle("go north"))   # Moving north
print(handle("take key"))   # Picking up key

Each case here matches a specific sequence shape, and the second element of the list is bound to a variable name (direction, item) usable inside that branch. This is closer to pattern matching in languages like Rust or Haskell than to a traditional switch statement.

Matching sequences and unpacking

Sequence patterns can fix some positions and capture the rest with a starred name, similar to unpacking assignment:

def summarize(items):
    match items:
        case []:
            return "empty"
        case [single]:
            return f"one item: {single}"
        case [first, second]:
            return f"two items: {first} and {second}"
        case [first, *rest]:
            return f"starts with {first}, {len(rest)} more after it"

print(summarize([]))            # empty
print(summarize([1]))           # one item: 1
print(summarize([1, 2, 3, 4]))  # starts with 1, 3 more after it

Patterns are also checked for length, so [first, second] only matches sequences with exactly two elements — a three-element list falls through to the next case, exactly as the length mismatch would suggest.

Matching class instances

match can inspect an object's class and pull out attributes in the same pattern, which is particularly useful for handling a family of related shapes, such as different kinds of events:

class Click:
    def __init__(self, x, y):
        self.x, self.y = x, y

class KeyPress:
    def __init__(self, key):
        self.key = key

def handle_event(event):
    match event:
        case Click(x=0, y=0):
            return "Clicked the origin"
        case Click(x=x, y=y):
            return f"Clicked at ({x}, {y})"
        case KeyPress(key="Escape"):
            return "Cancelled"
        case KeyPress(key=key):
            return f"Pressed {key}"

print(handle_event(Click(3, 4)))          # Clicked at (3, 4)
print(handle_event(KeyPress("Escape")))   # Cancelled

Each case both checks that event is an instance of the given class and destructures its attributes in one step, replacing what would otherwise be a chain of isinstance() checks followed by manual attribute access.

Guard conditions with if

An if clause after a pattern adds a condition that must also hold for the case to match, for logic that a pattern shape alone cannot express:

def classify(n):
    match n:
        case int() if n < 0:
            return "negative integer"
        case int() if n == 0:
            return "zero"
        case int():
            return "positive integer"
        case _:
            return "not an integer"

print(classify(-5))   # negative integer
print(classify(0))    # zero

If the pattern matches but the guard evaluates to False, Python moves on to the next case as if this one had not matched at all, rather than stopping the search.

Combining patterns with |

The | operator inside a pattern combines several alternatives into a single case, matching if any one of them applies:

def day_type(day):
    match day:
        case "Saturday" | "Sunday":
            return "weekend"
        case "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday":
            return "weekday"
        case _:
            return "not a day"

print(day_type("Sunday"))   # weekend

This reads more directly than an equivalent chain of separate cases, and mirrors how | is used elsewhere in Python's type hints to mean "one of these options," which keeps the meaning consistent across different parts of the language.

Patterns can also be nested and combined freely — a sequence pattern can contain a class pattern, which can itself contain an or-pattern, all in a single case line. This composability is exactly what separates structural pattern matching from a traditional switch statement: a switch only ever asks "does this value equal one of these constants," while match can ask "does this value have this shape, made of pieces that themselves have these shapes," in one readable expression.

As with any feature that adds expressive power, it is worth resisting the urge to force every conditional chain into a match statement. A plain if/elif chain remains perfectly appropriate, and often clearer, when the conditions being tested are unrelated boolean expressions rather than variations on the shape of one value. match earns its keep specifically when you are dispatching on what a value is or how it is structured.