
Exploring Python's New Structural Pattern Matching: Use Cases and Best Practices
Structural pattern matching (PEP 634) transforms how Python code expresses branching logic by matching shapes of data rather than awkward chains of if/elif. This post unpacks the syntax, demonstrates practical, real-world examples, and shows how to combine pattern matching with functools, itertools, and pytest for cleaner, faster, and more testable code.
Introduction
Python 3.10 introduced structural pattern matching, a powerful feature that lets you match data shapes (patterns) instead of checking individual conditions. Think of it like a smarter switch/case that understands lists, tuples, dicts, classes, and nested structures.
Why care? Because pattern matching can make code:
- More declarative and readable
- Easier to extend for new cases
- Safer by centralizing branching logic
Prerequisites: Python 3.10+ (structural pattern matching is available starting in 3.10). Familiarity with functions, classes, and basic standard library modules.
Prerequisites and Key Concepts
Before diving in, here's what you should understand:
- Python versions and compatibility (use 3.10+).
- Basic control flow (if/elif).
- Data structures: lists, tuples, dicts.
- Dataclasses/NamedTuple or simple classes for class patterns.
- Literal patterns: match constant values (e.g., 1, "ok").
- Capture patterns: bind parts of the pattern to names (e.g., x).
- Sequence patterns: match lists/tuples (e.g., [a, b, rest]).
- Mapping patterns: match dict-like objects (e.g., {"type": "point", "x": x}).
- Class patterns: match instances of classes and capture attributes.
- Guards: additional boolean checks on the matched pattern (e.g., if x > 0).
- OR patterns: alternative patterns joined by |.
- AS patterns: capture a value while matching a sub-pattern (e.g., point as p).
Core Concepts with Small Examples
Let's start small and build up.
Literal and Capture Patterns
def describe_value(v):
match v:
case 0:
return "zero"
case "": # literal empty string
return "empty string"
case int() as n: # matches ints and captures as n
return f"integer {n}"
case x:
return f"other: {x}"
Line-by-line:
match v:— starts a match block.case 0:— literal match for integer zero.case ""— literal match for empty string.case int() as n:— class pattern matching ints;ncaptures the int value.case x:— catch-all capture binding the whole value tox.
- Be careful:
case int()matches any int; ordering matters (put more specific literals before broad class patterns).
Sequence Patterns
def head_tail(seq):
match seq:
case []:
return "empty"
case [first]:
return f"one-element: {first}"
case [head, tail]:
return f"head={head}, tail={tail}"
Explain:
[]matches an empty sequence.[first]matches a sequence of length 1.[head, tail]captures first element and the rest as a list (works for lists or other sequence types supporting the protocol).
seq isn't a sequence type, a TypeError may occur. Use try/except or guards to handle different types.
Mapping Patterns
def handle_message(msg):
match msg:
case {"type": "add", "a": a, "b": b}:
return a + b
case {"type": "greet", "name": name}:
return f"Hello, {name}"
case {"type": t}:
return f"Unknown message type {t}"
case _:
return "Invalid message"
Explain:
- Matches dict-like objects. Unmatched keys are allowed unless you use extra checks.
- Useful for basic JSON-like dispatching.
Step-by-Step Real-World Examples
Now we'll tackle realistic patterns: command routing, parsing AST-like data, and a small evaluator.
Example 1 — Command Router for JSON-like Commands
Scenario: You receive JSON commands over a socket. Pattern matching makes the router clean.
from functools import lru_cache
import math
@lru_cache(maxsize=128)
def expensive_compute(x):
# Simulate a costly calculation
return math.sqrt(x) 3.14159
def route_command(cmd):
match cmd:
case {"cmd": "add", "a": a, "b": b}:
return a + b
case {"cmd": "sqrt_and_scale", "value": v}:
# cache repeated expensive computations
try:
return expensive_compute(v)
except TypeError:
raise ValueError("value must be a number")
case {"cmd": "echo", "payload": p}:
return p
case {"cmd": c}:
raise NotImplementedError(f"Unknown command {c}")
case _:
raise ValueError("Invalid command format")
Line-by-line notes:
@lru_cachefrom functools caches results ofexpensive_compute. This demonstrates integratingfunctoolsfor cleaner, faster code (memoization).route_commanddispatches on dict shapes.- Error handling: Type checking for numeric input and raising clear exceptions.
lru_cacherequires all args to be hashable. For mutable inputs, consider serializing or removing caching.
Example 2 — Parsing a Tiny Expression AST
Suppose you have an AST represented as nested tuples or dicts. Pattern matching excels here.
def evaluate(node):
match node:
case ("num", value):
return value
case ("add", left, right):
return evaluate(left) + evaluate(right)
case ("mul", left, right):
return evaluate(left) evaluate(right)
case ("neg", operand):
return -evaluate(operand)
case _:
raise ValueError("Unknown node")
Explain:
- The AST uses tuple shapes like ("add", left, right).
- Recursive evaluation shows how pattern matching reads the shape naturally.
- Use of recursion can be sped up with functools.lru_cache if nodes are hashable identifiers or if you memoize by node IDs.
- If many repeated sub-expressions exist, memoize results via node IDs or convert the AST to objects with stable identifiers.
Example 3 — Matching Dataclass/Custom Class Patterns
When matching custom types, structure patterns work well with dataclasses or classes that define __match_args__.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def classify_point(p):
match p:
case Point(0, 0):
return "origin"
case Point(x, 0):
return f"on x-axis at {x}"
case Point(0, y):
return f"on y-axis at {y}"
case Point(x, y) if x == y:
return "on diagonal"
case Point(x, y):
return "somewhere else"
Line-by-line:
- Dataclasses automatically support class pattern matching by using their positional fields.
- Guards (
if x == y) provide extra checks after matching structure.
- Custom classes without dataclass can define
__match_args__to control positional matching.
Combining itertools with Pattern Matching
The itertools module helps generate iterables efficiently; pair it with pattern matching for elegant solutions.
Example: find pairs of items that match a pattern.
from itertools import combinations
def find_pairs(items):
# items is a list of (type, value) tuples
result = []
for a, b in combinations(items, 2):
match (a, b):
case (("color", "red"), ("value", val)):
result.append(("red-value", val))
case (("name", n1), ("name", n2)) if n1 != n2:
result.append(("distinct_names", n1, n2))
case _:
continue
return result
Explain:
itertools.combinations(items, 2)efficiently iterates unique pairs.match (a, b)uses tuple sequence pattern matching to decode pair contents.- This approach avoids nested loops with manual unpacking and conditional checks.
- itertools works lazily; prefer it when working with large data streams.
Best Practices
- Use Python 3.10+ and test compatibility: include runtime checks if targeting multiple Python versions.
- Order matters: put specific patterns before general ones (e.g., literal before class patterns).
- Prefer explicit error handling for unexpected shapes: avoid silent fall-throughs.
- Keep matching logic pure where possible—use functions that return values rather than side effects.
- Use dataclasses or NamedTuple to make class pattern matching clear and robust.
- Use guards sparingly—keep them as helpers, not as primary logic.
- Consider memoization (functools.lru_cache) for expensive recursive match-based functions.
- Combine itertools for generating candidate structures to match when processing streams or combinatorial data.
Common Pitfalls and How to Avoid Them
- Confusing capture names with constants: unquoted names in patterns are captures; use literal patterns for constants by quoting or using value patterns like case 0 or case "x". For matching against a constant variable, use
case varname:won’t work as literal—usecase const_value:only for class patterns or use a guard (e.g.,case x if x == CONST). - Matching mutable types: sequence patterns expect sequence behavior—not arbitrary iterables. If your input could be any iterable, convert or check type.
- Performance: many deep nested matches have overhead—benchmark if performance is critical. Avoid heavy matching in tight loops; consider preprocessing or memoization.
- Overly clever patterns: clarity beats concision. If a pattern becomes hard to understand, break it into clearer functions.
Advanced Tips and Extensions
- __match_args__ and custom classes: define
__match_args__ = ("x", "y")to control which attributes are used for positional matching. - Dataclass + pattern matching: dataclasses are ideal; use
frozen=Truefor hashability if you plan to use instances as cache keys. - Use OR patterns:
case ("add", a, b) | ("sum", a, b):to handle synonyms. - AS patterns:
case point as p if isinstance(p, Point):captures whole object while also checking structure. - Integrate with functools.partial or singledispatch for routing patterns to handlers. Pattern matching can replace some use-cases for singledispatch by centralizing dispatch logic.
Testing Pattern Matching with Pytest
Example: We will write pytest tests for the evaluate AST evaluator from earlier.
Create a test module test_evaluator.py:
# test_evaluator.py
import pytest
from mymodule import evaluate # assume evaluator is in mymodule.py
@pytest.mark.parametrize("node,expected", [
(("num", 1), 1),
(("add", ("num", 2), ("num", 3)), 5),
(("mul", ("num", 2), ("add", ("num", 3), ("num", 4))), 14),
(("neg", ("num", 5)), -5),
])
def test_evaluate(node, expected):
assert evaluate(node) == expected
def test_invalid_node():
with pytest.raises(ValueError):
evaluate(("unknown",))
Explain:
- Use parametrization to cover multiple cases concisely.
- Test error conditions explicitly (
with pytest.raises) for unknown patterns. - For more complex match-based logic, add tests for guards, OR variants, and unexpected types.
- Cover representative shapes, nested structures, and edge cases.
- When using functools.lru_cache, tests should be written to ensure cache doesn't hide bugs; use cache clearing (
expensive_compute.cache_clear()) in fixtures if needed.
Performance Considerations
- Pattern matching is implemented in the interpreter; performance is generally comparable to equivalent if/elif structures. However:
- Memory considerations: lru_cache can hold references—be mindful of cache size.
Diagram (Described)
Imagine a flowchart:
- Start -> Receive data -> Pattern match dispatch:
This diagram maps how pattern matching centralizes dispatching: each branch checks a shape* (box) then a handler executes (box), with guarded checks as diamond decisions.
Common Questions (FAQ)
Q: Can I use pattern matching with asynchronous code? A: Yes. The match statement is orthogonal to async/await. Use pattern matching inside async functions and match on awaited results.
Q: Does matching mutate objects? A: No—matching only examines and binds names; it doesn't alter the matched objects.
Q: How to match against constants or module-level variables?
A: For a constant like MAX, use case _ if v == MAX: or use guarded checks. Names in patterns are capture names by default, not constants.
Conclusion
Structural pattern matching is a major addition to Python that encourages clearer, more declarative branching based on data shape instead of ad hoc if/elif checks. When combined with tools like:
- functools.lru_cache (for memoization and cleaner caching patterns),
- itertools (for efficient iteration and combination generation),
- pytest (for rigorous, parametrized test coverage),
Try it now:
- Convert one dispatch/if-heavy function to use pattern matching.
- Add tests with pytest covering the new patterns.
- If expensive recursive evaluation appears, add lru_cache to speed it up.
Further Reading and References
- PEP 634 — Structural Pattern Matching: Specification (official)
- Python docs — match statement (language reference)
- functools — Python Standard Library (lru_cache documentation)
- itertools — Python Standard Library (recipes and functions)
- pytest docs — Testing recommendations and fixtures
Happy pattern matching!
Was this article helpful?
Your feedback helps us improve our content. Thank you!