Python Nested Pattern Matching
python nested pattern: Learn how to use nested patterns in Python's match/case to destructure complex data structures with concise, readable code.
The python nested pattern feature in match/case lets you match against complex data structures without writing a series of isinstance checks and manual unpacking. Instead, you describe the shape you expect, and Python binds the values you need directly in the pattern. This article explains how nested patterns work, where they simplify code, and what to watch out for when using them in production.
What Nested Patterns Do in match/case
Python's match statement, introduced in 3.10, supports several pattern types: literal patterns, capture patterns, wildcard patterns, sequence patterns, mapping patterns, and class patterns. Nested patterns are simply patterns that appear inside another pattern. For example, a sequence pattern can contain a class pattern, and a class pattern can contain a mapping pattern. This lets you match a whole object graph in one statement.
Consider a simple configuration object:
from dataclasses import dataclass @dataclass class Server: host: str port: int
Without nested patterns, you might write:
def get_port(config): if isinstance(config, Server): return config.port return None
With a nested pattern, you can combine the type check and attribute extraction:
def get_port(config): match config: case Server(port=port): return port case _: return None
The Server(port=port) pattern matches any Server instance and binds its port attribute to the variable port. The pattern is nested because the class pattern is the outer pattern and the capture pattern port is nested inside it.
Sequence Patterns Inside Class Patterns
Nested patterns become more powerful when you combine different pattern types. Suppose you have a Point class and a Shape class that contains a list of points:
@dataclass class Point: x: float y: float @dataclass class Polygon: points: list[Point]
You can match a polygon and extract the first point's coordinates in one step:
def first_point(shape): match shape: case Polygon(points=[Point(x=x, y=y), *_]): return (x, y) case _: return None
The outer pattern is a class pattern for Polygon. Inside its points attribute, a sequence pattern matches a list whose first element is a Point with x and y captured. The *_ wildcard matches the rest of the list. This single pattern replaces multiple isinstance checks and indexing operations.
Mapping Patterns and Nested Keys
Mapping patterns match dictionaries by specifying keys. Nested mapping patterns let you reach into nested dictionaries without chaining .get() calls. For example, a request payload might look like this:
payload = { "user": { "name": "alice", "roles": ["admin", "editor"] } }
To extract the user's name and first role, you can write:
def extract_user(data): match data: case {"user": {"name": name, "roles": [first_role, *_]}}: return (name, first_role) case _: return None
The outer mapping pattern requires the key "user". Its value is another mapping pattern that requires "name" and "roles". The "roles" value is a sequence pattern that captures the first element. If any key is missing or the shape doesn't match, the case is skipped.
Combining Patterns with OR and Guards
Nested patterns can be combined with OR patterns (|) and guards (if) to express more complex conditions. For instance, you might want to match a point that lies on either axis:
def axis(point): match point: case Point(x=0, y=y) | Point(x=x, y=0): return "on axis" case _: return "off axis"
Guards can refine nested patterns. Suppose you want to match a polygon with exactly three points:
def triangle(shape): match shape: case Polygon(points=[Point(), Point(), Point()]) if len(shape.points) == 3: return "triangle" case _: return "not triangle"
The guard runs after the pattern matches, so you can check properties that aren't directly captured. Note that the pattern itself already requires three points, so the guard is redundant here, but it illustrates how guards work with nested patterns.
Common Mistakes with Nested Patterns
Nested patterns are concise, but they have subtle pitfalls. One common mistake is using a capture pattern in a position where you intended a literal. For example, case Point(x=0, y=0) matches a point at the origin, but case Point(x=x, y=y) captures any point. The variable name x shadows the outer variable, which can lead to confusion.
Another mistake is forgetting that sequence patterns match any iterable, not just lists. A tuple of points will also match [Point(), Point()]. If you need to distinguish between list and tuple, you can use a class pattern like list(points=[...]) or check the type explicitly.
Mapping patterns only match exact keys by default. If a dictionary has extra keys, the pattern still matches as long as the specified keys are present. To require an exact set of keys, you need a guard or a custom matcher.
Finally, nested patterns can become deeply indented and hard to read. When a pattern spans multiple lines, consider breaking it into smaller named patterns or using a helper function to keep the match statement readable.
Performance and Maintainability
The match statement is compiled into efficient bytecode that avoids repeated attribute lookups and isinstance checks. Nested patterns are evaluated lazily: Python checks the outer pattern first and only descends into inner patterns when the outer shape matches. This means a deeply nested pattern that fails early doesn't pay the cost of checking the inner structure.
That said, very large data structures can still incur a cost proportional to the depth of the pattern. For a one-off script, this is rarely a concern. For hot paths, you should profile rather than assume. The main benefit of nested patterns is maintainability: they reduce the number of branches and make the expected shape explicit. Code that uses nested patterns is often easier to modify when the data structure changes, because the pattern is localized.
When to Use Nested Patterns vs. Manual Unpacking
Nested patterns shine when you need to validate and extract from a known structure in a single step. Use them when the shape is stable and you want to avoid repetitive boilerplate. They are especially useful for parsing JSON-like data, handling command-line arguments, or implementing state machines.
Manual unpacking with isinstance, get, and indexing is still appropriate when the structure is highly dynamic or when you need to perform additional logic that doesn't fit a pattern. For example, if you need to mutate the data while inspecting it, a pattern match is not the right tool. Also, if you are targeting Python versions before 3.10, nested patterns are unavailable, so manual unpacking remains necessary.
When you do use nested patterns, keep them shallow. A pattern that nests more than three levels deep is usually a sign that the data structure itself is too complex and could benefit from refactoring. Prefer named patterns or sub-patterns to improve readability.
Nested patterns are a powerful addition to Python's control flow. They let you express complex destructuring in a declarative way, reducing the chance of off-by-one errors and missing edge cases. By understanding how they compose and where they fall short, you can use them effectively without turning your code into a maze of brackets and underscores.