Back to Blog
Python

Python Structural Pattern Matching Explained

Python structural pattern matching lets you write case-based code that destructures data and binds variables. This guide covers match syntax, guards, mappings, classes, and common pitfalls.

pattern matchingmatch statementcontrol flowPython 3.10syntax
Illustration of a Python match statement with branching paths representing pattern matching cases

Python structural pattern matching, introduced in Python 3.10, adds a match statement that lets you compare a value against a series of patterns and execute code based on which pattern matches. Unlike a simple if/elif chain, pattern matching can destructure data, bind variables, and apply guards in a single construct. This article explains the syntax, shows realistic usage, and highlights where the feature shines and where it can trip you up.

The match Statement and Its Basic Syntax

The match statement evaluates an expression and compares the result against one or more case clauses. Each case defines a pattern, and if the pattern matches, the corresponding block runs. A minimal example looks like this:

def describe(value): match value: case 0: return "zero" case 1: return "one" case _: return "something else"

The underscore _ is a wildcard that matches anything. The match statement does not fall through like C-style switch statements; only the first case whose pattern and guard both match executes. This makes it a clean replacement for long if/elif chains when the comparisons are structural rather than purely boolean.

Matching Literals and Capturing Variables

Literal patterns compare against exact values. You can match integers, strings, booleans, and None. More usefully, you can bind parts of the matched value to variables using capture patterns:

def parse_command(command): match command.split(): case ["quit"]: return "exiting" case ["hello", name]: return f"hello {name}" case ["add", a, b]: return int(a) + int(b) case _: return "unknown command"

Here name, a, and b are captured from the list pattern. The capture pattern binds the matched value to the variable name, which is then available in the case block. This eliminates the need to manually index the list after splitting.

Matching Sequences and Mappings

Sequence patterns match against lists, tuples, and other sequences. You can specify exact lengths or use * to capture the rest of the sequence:

def process_items(items): match items: case []: return "empty" case [first]: return f"one item: {first}" case [first, *rest]: return f"first: {first}, rest: {rest}"

Mapping patterns work with dictionaries. You can match on specific keys and bind their values:

def handle_request(request): match request: case {"method": "GET", "path": path}: return f"GET {path}" case {"method": "POST", "data": data}: return f"POST with {data}" case _: return "unsupported"

Mapping patterns only require the listed keys to be present; extra keys are ignored. This is useful when you only care about a subset of a dictionary's fields.

Matching Objects and Classes

Pattern matching also works with class instances. You can match on the class and bind attributes using the class_name(attribute=pattern) syntax:

class Point: __match_args__ = ("x", "y") def __init__(self, x, y): self.x = x self.y = y def locate(point): match point: case Point(0, 0): return "origin" case Point(x, y): return f"({x}, {y})" case _: return "not a point"

By defining __match_args__, you allow positional matching. Without it, you must use keyword patterns like Point(x=x, y=y). This feature is especially valuable when working with dataclasses or namedtuples, as they automatically support structural matching.

Using Guards to Add Conditions

A guard is an if clause attached to a case that must evaluate to True for the case to match. Guards let you add arbitrary conditions without resorting to nested if statements:

def classify(number): match number: case n if n > 0: return "positive" case n if n < 0: return "negative" case 0: return "zero"

Guards are evaluated only after the pattern itself matches. They are a natural place for range checks, type checks, or any condition that depends on the captured variables.

Combining Patterns and Using Wildcards

You can combine multiple patterns with the OR operator | inside a single case:

def is_weekend(day): match day: case "Saturday" | "Sunday": return True case _: return False

The wildcard _ matches anything and is often used as the default case. You can also use a capture pattern to bind the entire value, but if you need the value in the default case, use a variable name instead of _:

case other: return f"unexpected: {other}"

This binds the matched value to other, which can be useful for logging or error handling.

Common Pitfalls When Using Match

One frequent mistake is expecting fall-through behavior. Python's match does not fall through; only the first case whose pattern and guard both match executes. Another pitfall is accidentally shadowing existing variables: a bare name in a case pattern is a capture pattern, so case x binds the entire value to x, overwriting any previous value in the current scope. To compare against the value of an existing variable, use a guard, not a bare name:

value = 5 match something: case _ if something == value: ...

Here the wildcard _ matches any value and the guard decides whether the case runs. A bare case value: would bind rather than compare, so it would match unconditionally.

Performance and Maintainability Considerations

Pattern matching can make complex nested if chains flatter and easier to read, especially when the logic mirrors the data shapes your program uses. That readability benefit is often the main reason to use it. Pattern matching is not a performance magic bullet, however; its runtime cost depends on the interpreter, the complexity of the patterns, and the surrounding code. If performance matters in a hot path, profile before replacing existing logic.

Maintainability improves when patterns mirror the data structures your program uses. If your data shapes change, updating the patterns is often easier than updating a series of if checks. That said, overusing pattern matching for trivial comparisons can reduce readability, especially for developers unfamiliar with the syntax. Use it where it genuinely reduces complexity.

One operational concern is compatibility: match requires Python 3.10 or later. Code that uses it will raise a SyntaxError on older interpreters, so check your project's supported Python versions before adopting it.

Python Structural Pattern Matching: Syntax, Examples, and Pitfalls | RYUSLOG DEV