Advanced Structural Pattern Matching
If you have read our guide to the basics of match/case, you already know how to match literals, capture variables, and use guards. But Python’s pattern matching is far more powerful than that. This guide takes you into the advanced territory: mapping patterns, sequence patterns with stars, custom class matching, and practical applications like state machines.
Mapping Patterns
Mapping patterns let you match against dictionaries and other mapping objects. Unlike class patterns (which rely on specific types), mapping patterns work with any object supporting keys() and getitem:
def parse_config(config):
match config:
case {"host": host, "port": port}:
return f"Server at {host}:{port}"
case {"mode": mode} if mode in ("fast", "safe"):
return f"Mode: {mode}"
case {"debug": True, **rest}:
return f"Debug enabled, extra: {rest}"
case _:
return "Unknown config"
parse_config({"host": "localhost", "port": 8080})
# -> "Server at localhost:8080"
parse_config({"debug": True, "verbose": True})
# -> "Debug enabled, extra: {verbose: True}"
The **rest pattern captures any remaining key-value pairs into a dictionary—useful when you want to be flexible about extra fields.
Restricting Keys
You can use keyword-only patterns to require certain keys while ignoring others:
def process_request(req):
match req:
case {"method": "GET", "path": path, **}:
return f"GET {path}"
case {"method": "POST", "path": path, "body": body, **}:
return f"POST {path} with body"
case _:
return "Invalid request"
The bare ** (without a variable name) silently absorbs extra keys without capturing them.
Sequence Patterns with Stars
Sequence patterns match lists, tuples, and other iterables. The star pattern (*) lets you capture the rest of a sequence:
def categorize_list(items):
match items:
case []:
return "Empty"
case [x]:
return f"Single element: {x}"
case [first, *middle, last]:
return f"First: {first}, Last: {last}, Middle: {middle}"
case _:
return "Two or more elements"
categorize_list([1, 2, 3, 4, 5])
# -> "First: 1, Last: 5, Middle: [2, 3, 4]"
This is particularly useful for command-line arguments or file path manipulation.
Mixed Sequence and Class Patterns
You can combine sequence and class patterns for complex data:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
@dataclass
class Circle:
center: Point
radius: int
def describe_shape(shape):
match shape:
case Point(0, 0):
return "Origin point"
case Point(x=0, y=y):
return f"Point on Y-axis at y={y}"
case Point(x=x, y=0):
return f"Point on X-axis at x={x}"
case Circle(center=Point(x=0, y=0), radius=r):
return f"Circle at origin with radius {r}"
case Circle():
return "Some other circle"
describe_shape(Circle(Point(0, 0), 10))
# -> "Circle at origin with radius 10"
Custom Class Matching
Python gives you fine-grained control over how your classes match. Three special attributes shape the matching behavior.
match_args
Define which attributes are matched positionally:
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
city: str
# Only match on name and age, in that order
__match_args__ = ("name", "age")
def greet(person):
match person:
case Person("Alice", age=30):
return "Hi Alice, you are 30!"
case Person(name, age):
return f"Hi {name}, you are {age}!"
case _:
return "Not a person"
greet(Person("Alice", 30, "London"))
# -> "Hi Alice, you are 30!"
greet(Person("Bob", 25, "Paris"))
# -> "Hi Bob, you are 25!"
With match_args, the pattern Person(“Alice”, 30) matches the name and age, ignoring the city.
match_primitive
Control how your object matches against primitive values:
class Version:
def __init__(self, major: int, minor: int, patch: int = 0):
self.major = major
self.minor = minor
self.patch = patch
def __match_primitive__(self):
return (self.major, self.minor, self.patch)
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
def check_version(ver):
match ver:
case Version(3, 10):
return "Python 3.10"
case Version(3, minor) if minor >= 10:
return f"Python 3.{minor}+"
case Version(major) if major >= 3:
return f"Python {major}.x"
case _:
return "Unknown version"
check_version(Version(3, 10, 1))
# -> "Python 3.10"
check_version(Version(3, 11))
# -> "Python 3.11+"
This is useful for value objects that should match on their underlying representation.
match_always
You can make a class only match as a class pattern (never as a mapping or sequence) by raising an exception in a custom method. But by default, dataclasses and namedtuples can match both positionally and with keywords.
Matching Specific Types
Sometimes you need to match not just the structure but the type itself:
def describe_value(value):
match value:
case int() as i if i > 0:
return f"Positive integer: {i}"
case int():
return f"Non-positive integer: {value}"
case float() as f:
return f"Float: {f}"
case str() as s if len(s) > 10:
return f"Long string: {s[:10]}..."
case str():
return f"String: {value}"
case list():
return f"List with {len(value)} items"
case _:
return "Unknown type"
describe_value(42) # -> "Positive integer: 42"
describe_value(-5) # -> "Non-positive integer: -5"
describe_value(3.14) # -> "Float: 3.14"
describe_value("hello") # -> "String: hello"
The TypeName() pattern matches any value of that type and captures it with as.
Real-World: State Machines
Pattern matching excels at implementing state machines. Here is a simplified HTTP request parser:
from dataclasses import dataclass
from enum import Enum, auto
class State(Enum):
IDLE = auto()
READING_METHOD = auto()
READING_PATH = auto()
READING_VERSION = auto()
READING_HEADERS = auto()
DONE = auto()
@dataclass
class Parser:
state: State
method: str = ""
path: str = ""
headers: dict = None
def __post_init__(self):
if self.headers is None:
self.headers = {}
def parse_http(request: str):
parser = Parser(State.IDLE)
for line in request.split("\\n"):
match parser.state, line.split():
case State.IDLE, [method]:
parser = Parser(State.READING_METHOD, method=method)
case State.READING_METHOD, [path]:
parser = Parser(State.READING_PATH, method=parser.method, path=path)
case State.READING_PATH, [version] if version.startswith("HTTP/"):
parser = Parser(State.READING_HEADERS, method=parser.method, path=parser.path)
case State.READING_HEADERS, [key, value]:
parser.headers[key.rstrip(":")] = value
case State.READING_HEADERS, []:
parser.state = State.DONE
case _:
pass # Ignore malformed lines
return parser
http_request = """GET /index.html HTTP/1.1
Host: example.com
User-Agent: Mozilla
"""
result = parse_http(http_request)
print(f"Method: {result.method}, Path: {result.path}")
# -> Method: GET, Path: /index.html
This pattern scales well for protocol parsers, game engines, and any system with discrete states.
Performance Considerations
Pattern matching is optimized, but be mindful of a few things:
-
Order matters: Put the most common cases first. The matcher checks patterns sequentially until one matches.
-
Specific beats general: More specific patterns are checked faster than general ones because they can eliminate non-matches earlier.
-
Guards add overhead: Every guard adds a conditional check. If possible, restructure your patterns to avoid guards.
-
Avoid capturing expensive operations: The pattern case [expensive_func(), *rest]: calls the function even if it is not needed.
See Also
match-case-guide— The fundamentals of Python’s match/case statementpython-dataclasses— Create structured data classes that work great with pattern matchingpython-enum— Enum types for defining fixed sets of values