Advanced Structural Pattern Matching

· 5 min read · Updated March 16, 2026 · advanced
python pattern-matching advanced control-flow

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:

  1. Order matters: Put the most common cases first. The matcher checks patterns sequentially until one matches.

  2. Specific beats general: More specific patterns are checked faster than general ones because they can eliminate non-matches earlier.

  3. Guards add overhead: Every guard adds a conditional check. If possible, restructure your patterns to avoid guards.

  4. 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 statement
  • python-dataclasses — Create structured data classes that work great with pattern matching
  • python-enum — Enum types for defining fixed sets of values