Use Python Structural Pattern Matching

Learn Python's match-case statement: destructuring sequences, matching mappings, using guards, capturing values, and replacing complex if-elif chains.

8 min read

Structural pattern matching, introduced in Python 3.10, is the match-case statement. It lets you compare a value against patterns that describe its shape, not just its equality to constants. You can destructure sequences, match dictionary keys, check class types while binding attributes, and add conditional guards.

The match statement replaces deeply nested if-elif chains that check types, lengths, and key membership. Where you used to write isinstance calls followed by indexing and attribute access, you now write a pattern that declares the structure you expect.

Pattern matching is especially useful for processing JSON-like data, handling AST nodes in compilers, implementing parsers, and building command dispatchers. It makes code that deals with structured data dramatically more readable.

The article on advanced Python mistakes to avoid covers other cases where a simpler, more declarative tool beats a chain of manual checks.

Matching simple values and sequences

The simplest patterns match against literal values. More useful patterns destructure sequences, binding elements to names as part of the match.

pythonpython
def describe(command):
    match command:
        case ["help"]:
            return "Showing help"
        case ["greet", name]:
            return f"Hello, {name}"
        case ["add", a, b]:
            return int(a) + int(b)
        case _:
            return "Unknown command"
 
print(describe(["help"]))
print(describe(["greet", "Maya"]))
print(describe(["add", "3", "4"]))

Each case describes a specific list shape. The elements are bound to names automatically. The underscore is the wildcard that matches anything.

texttext
Showing help
Hello, Maya
7

The star pattern captures all remaining elements beyond the first into a new list. This approach handles variable-length sequences gracefully without requiring an exact count of elements to be specified.

pythonpython
def process(items):
    match items:
        case [first, *rest]:
            print(f"First: {first}, Rest: {rest}")
 
process([1, 2, 3, 4])

The first element is bound to the name first as a single value. All remaining elements are collected into a newly created list bound to the name rest for further processing.

texttext
First: 1, Rest: [2, 3, 4]

Matching mappings and dictionaries

Mapping patterns match dictionaries by their keys. You specify the keys you care about and bind their values to names. Extra keys are ignored by default.

pythonpython
def handle_event(event):
    match event:
        case {"type": "click", "x": x, "y": y}:
            return f"Clicked at ({x}, {y})"
        case {"type": "keypress", "key": key}:
            return f"Pressed {key}"
        case _:
            return "Unknown event"
 
print(handle_event({"type": "click", "x": 100, "y": 200}))
print(handle_event({"type": "keypress", "key": "Enter"}))

Only the specified keys are matched. Any additional keys in the dictionary are ignored. Missing required keys cause the pattern to fail.

texttext
Clicked at (100, 200)
Pressed Enter

Matching class instances

Class patterns check the type of a value and bind its attributes in a single step. This replaces the pattern of calling isinstance followed by manual attribute access.

pythonpython
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
 
class Circle:
    def __init__(self, center, radius):
        self.center = center
        self.radius = radius
 
def area(shape):
    match shape:
        case Circle(center=Point(x=x, y=y), radius=r):
            return 3.14 * r * r
        case _:
            return 0
 
c = Circle(Point(10, 20), 5)
print(area(c))

The pattern matches nested class structures and binds deeply nested attributes to names. The entire isinstance check, attribute access, and binding happens declaratively.

texttext
78.5

The class pattern uses keyword arguments to match attributes. If the class uses match_args, positional matching is also supported for simpler cases.

Adding guards for conditional matching

A guard is an if clause after a pattern. The pattern must match and the guard must evaluate to True for the case to execute. This adds conditional logic without nesting.

pythonpython
def categorize(number):
    match number:
        case n if n < 0:
            return "negative"
        case n if n == 0:
            return "zero"
        case n if n % 2 == 0:
            return "even"
        case _:
            return "odd"
 
print(categorize(-5))
print(categorize(0))
print(categorize(4))
print(categorize(7))

Each case matches any number but the guard restricts which numbers actually reach the body. The wildcard catches anything not filtered by earlier guards.

texttext
negative
zero
even
odd

OR patterns for combining alternatives

The pipe operator combines multiple patterns into a single case. If any of the alternatives match, the case executes. This eliminates duplicate case bodies.

pythonpython
def is_special(code):
    match code:
        case 200 | 201 | 204:
            return True
        case 400 | 404 | 405:
            return False
        case _:
            return None
 
print(is_special(200))
print(is_special(404))
print(is_special(500))

Three status codes share the same handling. The OR pattern keeps the logic in one place instead of copying it across multiple case blocks.

texttext
True
False
None

The OR pattern works with all pattern types, including sequence patterns, mapping patterns, and class patterns. Each alternative is tried in order until one matches or all fail.

The article on special methods in Python covers the dunder methods, like __eq__ and __repr__, that classes used in patterns typically also define.

Rune AI

Rune AI

Key Insights

  • match-case destructures data by shape, not just by value like a switch statement.
  • Sequence patterns match lists and tuples; use *rest to capture remaining elements.
  • Mapping patterns match dicts by key; use **rest to capture remaining entries.
  • Class patterns match by type and bind attributes in one step.
  • Guard clauses (if after a pattern) add conditional logic without nesting.
RunePowered by Rune AI

Frequently Asked Questions

Is Python's match-case the same as a switch statement?

No. Python's match-case is structural pattern matching, which is far more powerful than a simple switch. It can destructure sequences, match against mapping keys, check class types while binding attributes, apply guards (conditional if clauses), and combine patterns with OR. A switch statement only compares a value against constants. Match-case compares a value against complex structural patterns.

When should I use match-case instead of if-elif?

Use match-case when you are branching based on the structure of data rather than simple boolean conditions. It is ideal for handling JSON-like nested data, processing AST nodes, implementing state machines, parsing command patterns, and any situation where you check both the type and the contents of a value simultaneously. Stick with if-elif for simple boolean conditions and range checks.

Conclusion

Structural pattern matching is one of the most significant Python syntax additions in years. It turns nested isinstance checks and dictionary key lookups into readable, declarative patterns. Start with simple value matching and sequence destructuring. Add class patterns when you process typed data. Use guards when logic goes beyond structural shape. The match statement rewards you with code that reads like a description of the data you expect.