Python Structural Pattern Matching Explained
python structural pattern matching: Learn how Python structural pattern matching works with practical examples covering literals, sequences, mappings, classes, guards,...
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 matching case 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 matching case executes. Another pitfall is using capture patterns in a way that accidentally shadows existing variables. For example, case x will bind the entire value to x, overwriting any previous value. Also, remember that case patterns are literal unless you use a capture pattern. If you want to compare against a variable's value, you must use a guard:
value = 5 match something: case value: # This binds, not compares! ... case _ if something == value: ...
The first case binds something to value, so it always matches. To compare, use a guard or a literal pattern.
Performance and Maintainability Considerations
Pattern matching is implemented efficiently in CPython, but the real benefit is often readability and maintainability. Complex nested if chains become flat case clauses, making the intent clearer. However, pattern matching is not a performance magic bullet. For simple integer comparisons, a match is comparable to an if/elif chain, but it adds overhead when patterns are complex and involve destructuring. If you are in a tight loop, 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. If your project must support older Python versions, you cannot use this feature without a backport or a transpiler. This is a critical deployment consideration for libraries and applications with a broad user base.