Language syntax4 min read

Python Pattern Matching Tests Structure and Binds Names

How match and case handle sequence, mapping, class, capture, value, and guarded patterns in Python 3.10 and later.

  • pattern matching
  • control flow
  • data models

Python’s match statement tries case patterns in order. A successful pattern can bind parts of the subject to names, then an optional guard decides whether that case runs. It does not perform destructuring assignment, and most patterns are not equality tests.

Structural pattern matching arrived in Python 3.10. This article targets Python 3.14.7 and follows the language reference for match and the normative PEP 634 specification.

Bare names capture instead of compare

NOT_FOUND = 404

def classify(status):
    match status:
        case 200:
            return "ok"
        case NOT_FOUND:
            return f"captured {NOT_FOUND}"

This function does not compare with the module constant. NOT_FOUND is a capture pattern, so it matches every value other than 200 and binds that value to a new local name. A later case would be unreachable and would make the function a syntax error. Literal patterns compare against literal values. A dotted value pattern performs lookup and compares with ==:

from http import HTTPStatus

def classify(status):
    match status:
        case HTTPStatus.OK:
            return "ok"
        case HTTPStatus.NOT_FOUND:
            return "missing"
        case other:
            return f"status {other}"

The wildcard _ also matches anything, but it does not bind a name. An unguarded capture or wildcard is irrefutable, so it must be the final case.

Sequence patterns use sequence eligibility rules

def command(parts):
    match parts:
        case ["move", source, destination]:
            return ("move", source, destination)
        case ["tag", item, *labels]:
            return ("tag", item, labels)
        case _:
            raise ValueError("unsupported command")

assert command(["move", "a", "b"]) == ("move", "a", "b")
assert command(("tag", "issue-7", "bug", "urgent")) == (
    "tag", "issue-7", ["bug", "urgent"]
)

Square and round pattern syntax have the same sequence semantics. A starred subpattern receives a new list. Eligibility is narrower than general iteration: generators and arbitrary objects with __iter__ do not qualify. str, bytes, and bytearray are deliberately excluded even though they are sequences elsewhere in Python.

Mapping patterns accept extra keys

def event_name(event):
    match event:
        case {"type": "rename", "name": str(name), **rest}:
            return name, rest
        case {"type": event_type}:
            return event_type, {}
        case _:
            raise ValueError("event has no type")

assert event_name({"type": "rename", "name": "report", "id": 8}) == (
    "report", {"id": 8}
)

A mapping pattern requires its listed keys but ignores other keys unless **rest captures them. It uses the subject’s two-argument get() method. It does not trigger defaultdict.__missing__ to manufacture absent keys.

Class patterns read attributes

A class pattern first uses isinstance. Keyword subpatterns then read named attributes. Positional subpatterns are translated through the class’s __match_args__ tuple.

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

def quadrant(point):
    match point:
        case Point(0, 0):
            return "origin"
        case Point(x, y) if x > 0 and y > 0:
            return "upper right"
        case Point(x=x, y=y):
            return f"other ({x}, {y})"
        case _:
            raise TypeError("expected Point")

Dataclasses generate __match_args__ from non-keyword-only fields that participate in the generated initializer. For a public matching API, keyword patterns are more stable because reordering __match_args__ can silently change what positional patterns mean.

Attribute access during matching can run properties and descriptors. An AttributeError makes that class pattern fail. Other exceptions propagate. Patterns should not be treated as side-effect-free reflection.

Guards run after binding

A guard runs only after its pattern succeeds, with all capture names available. If the guard is false, matching continues with the next case. The reference guarantees case order and guard evaluation order, but it permits implementations to cache some value lookups and lengths.

Names bound by a successful selected case remain in the surrounding local scope after the match. For failed patterns, the specification deliberately leaves partial bindings unspecified. Do not inspect or reuse them.

Use match when the branch condition is the shape of data and the captures are useful to the branch. Use if and elif when the decision is mainly an arbitrary boolean predicate. That keeps hidden attribute reads and name binding out of code that does not benefit from structural patterns.