March 1, 2021·5 min read·engineeringpythonpatternslanguage-design

Python's sixth alpha exposes structural pattern matching

Python 3.10.0a6 makes structural pattern matching available for evaluation. Patterns combine shape checks with binding, which introduces rules that ordinary expressions do not have.

Code that examines a syntax tree often spends more space establishing the shape of a node than performing the intended transformation. A branch checks a class, inspects an attribute, checks another class, and then extracts the values it needs. Python 3.10.0a6, released on March 1, includes structural pattern matching, which gives that repeated work a language construct. The interesting change is the combination of conditional branching and data extraction.

The design rationale in PEP 635 describes this problem through nested binary-expression nodes. Selecting an operation can depend on both the class of a node and the values inside its children. A method dispatched on the outer class alone cannot express all those conditions. Pattern matching lets the caller describe a nested shape and bind its components in the same statement.

I like Python's willingness to let ordinary objects participate in common protocols. Pattern matching extends that approach, but it also adds a substantial body of semantics that readers must learn. Its usefulness depends on whether the data has enough structure to justify those rules.

Matching also extracts values

The tutorial included in the alpha's source explains how a subject passes through successive cases. The first successful case executes. A pattern may compare literal values, capture components, or combine those operations. If no case succeeds, none of the case bodies runs.

A command represented as a sequence makes the distinction visible. These examples are for the new alpha syntax.

def describe(command):
    match command:
        case ["move", x, y]:
            return ("destination", x, y)
        case ["stop"]:
            return ("stopped",)
        case _:
            raise ValueError("Unsupported command")

The first pattern checks the sequence shape and the literal first element. Its other names bind the second and third elements. A successful match therefore supplies the values the case body needs. The pattern does not state that the coordinates are integers, and the binding operation does not convert them. An application that requires numeric coordinates still needs to express that requirement. Case ordering remains part of the algorithm because an earlier successful pattern prevents later cases from running.

This sequence pattern matches suitable sequences regardless of whether the pattern uses square brackets or parentheses. The tutorial expressly excludes strings and iterators. That restriction avoids treating every iterable as a destructurable sequence, which would otherwise make the treatment of text and consumable iterators difficult to predict. A generator containing three values is not interchangeable with a list for this operation.

Names inside patterns have different jobs

A standalone name captures a value. It does not compare the subject with a variable that happens to exist under the same name. The distinction is easy to miss because ordinary Python expressions resolve names to their current values. Pattern syntax uses the same spelling for a different operation.

The tutorial requires dotted names for named constants. An enum member such as Color.RED expresses a value to compare, while a bare RED is a capture. The wildcard _ accepts a subject without binding it. These rules let a pattern mix literal constraints and extraction, but they make an expression-based reading unreliable.

from enum import Enum

class State(Enum):
    READY = 1
    STOPPED = 2

def allowed(state):
    match state:
        case State.READY:
            return True
        case _:
            return False

The dotted member gives the first case its comparison semantics. Replacing it with a bare name would change the meaning of the pattern. This is a language rule that deserves direct explanation in code review because indentation and familiar variable names cannot communicate it on their own.

PEP 635 also explains why match and case are soft keywords. Existing programs commonly use match as an identifier, so reserving it everywhere would create avoidable compatibility problems. The parser recognizes its keyword role in the appropriate grammatical position. Code that already uses that name does not need a global rename merely because the language gains the statement.

Shape matching is not a complete schema

Mapping patterns select required keys and can capture their associated values. Unlike fixed-length sequence patterns, they allow additional keys by default. That difference supports looking for a relevant part of a larger record. It also means a successful match does not prove that a message contains only the fields the application expects.

def destination(message):
    match message:
        case {"kind": "move", "x": x, "y": y} if x >= 0 and y >= 0:
            return (x, y)
        case _:
            raise ValueError("Unsupported destination")

The guard runs after the pattern has bound x and y. If it is false, matching continues with the next case. The comparisons in this example assume values that support the requested ordering. A mapping with unsuitable coordinate values can raise an exception in the guard, so this function is not a general validator for arbitrary untrusted input.

Guards provide relationships that the pattern itself does not describe. PEP 635 uses comparisons between captured elements to explain the need. Keeping the structural condition and the ordinary expression distinguishable makes it possible to see which part extracts values and which part applies an application rule. The guard remains ordinary executable Python, with the behavior and possible effects that implies.

Classes can expose their destructuring rules

Class patterns inspect objects through a class name and attribute patterns. Keyword patterns identify attributes directly. Positional patterns depend on the class's __match_args__ declaration, which maps positions to attribute names. The tutorial describes how a declaration containing "x" and "y" makes positional and keyword forms correspond.

That declaration becomes relevant to callers who destructure the class. Changing the order would change what a positional pattern means, even when a reader sees the same class name and the same number of components. Keyword patterns communicate the selected attributes without requiring the reader to recover that order. For public classes, the choice affects how independently written consumers express their assumptions.

I am more persuaded by matching structured messages and tree nodes than by replacing small literal comparisons. In those cases, the pattern removes repeated shape checks and keeps the captured data beside its conditions. A branch that only compares one scalar gains less from the extra syntax.

This remains an alpha release, so evaluating the implementation and its documentation is part of the work. The most useful examples are ones where nesting previously required several dependent checks. They expose whether a pattern makes those dependencies clear, whether a guard contains additional assumptions, and how the object model supplies the attributes being matched.