October 4, 2021·6 min read·engineeringpythonpatternsprotocolsdiagnostics

Python 3.10 makes object structure part of control flow

Python 3.10 adds structural pattern matching and more precise syntax diagnostics. Its matching rules expose important choices about protocols, attribute access and public class interfaces.

Inspecting structured data often requires several dependent operations before the useful work begins. Code checks the outer class, finds a child, checks that child's shape, and extracts the relevant values. Python 3.10.0, released on October 4, introduces structural pattern matching to describe those dependencies together. The feature gives object structure a direct role in selecting a block of code.

The motivation is broader than selecting among literal constants. PEP 635 discusses syntax-tree processing, where an operation can depend on nested nodes and their attributes. Dispatching a method on one outer class does not describe every such relationship. A pattern can identify the nested structure and bind the values needed by the selected operation.

I regard Python's protocol-based object model as one of its strongest attributes. Pattern matching extends that model into control flow, so its protocol choices matter as much as the new keywords. Reading a pattern requires knowing which operations it asks an object to support and which assumptions it leaves to the case body.

A successful pattern establishes specific facts

The specification in PEP 634 defines a subject expression followed by ordered cases. The first case whose pattern succeeds and whose optional guard is true executes. A case can constrain literal values while binding other components. An unmatched subject executes no case body unless a final wildcard accepts it.

def route(message):
    match message:
        case {"kind": "move", "position": [x, y]}:
            return ("move", x, y)
        case {"kind": "stop"}:
            return ("stop",)
        case _:
            raise ValueError("Unsupported message")

This function recognizes selected mapping shapes. Its first pattern requires the named keys and a two-element sequence under position. It captures the components without specifying their numeric types. A successful match establishes those particular structural facts, so the case body must not assume it has performed a complete validation of the message.

Extra mapping keys do not cause that pattern to fail. A mapping pattern describes required entries, which is useful when a larger message contains unrelated metadata. A protocol that prohibits unknown fields needs a separate rule. Otherwise, a developer can mistake successful extraction for acceptance of an exact schema.

Sequence patterns have a different default. A fixed-length pattern requires a matching length, while a starred component can absorb remaining elements into a list. Strings, bytes and bytearrays do not match sequence patterns even though other language operations regard them as sequences. An iterable interface by itself is also insufficient. These exclusions prevent a short pattern from silently treating text or a consumable iterator like an ordinary list.

Pattern names are not ordinary expressions

An unqualified name captures a value. It does not compare against a variable with the same spelling. A dotted name expresses a value pattern and uses the value found through ordinary name lookup for its comparison. This distinction gives patterns their extraction behavior but requires readers to interpret names according to their grammatical role.

An OR pattern can combine alternatives, and each alternative must bind the same set of names. That requirement gives the selected case body a consistent collection of captures regardless of which alternative matched. The wildcard _ accepts a value without creating a binding, so it is appropriate when the program intentionally ignores a component.

Guards are ordinary expressions evaluated after a successful structural match. A false guard lets matching proceed to later cases, while an exception in the guard propagates. A comparison such as x > 0 can express a condition that the pattern grammar does not encode, but it still requires x to support that comparison. The guard does not turn unsuitable data into a harmless failed match automatically.

Bindings from a successful match remain available after the statement. Failed partial matches have deliberately unspecified binding behavior. Code must neither rely on a partially captured value remaining nor assume that every previous binding is unchanged. Using captures within the successful case keeps their intended relationship to the matched data clear.

Class patterns create a public interpretation of attributes

A class pattern begins with an instance check. Keyword patterns then look up named attributes and match their values. AttributeError causes that class pattern to fail, while another exception raised by attribute access propagates. A property can therefore participate in matching, and its implementation can affect the outcome beyond merely returning a stored field.

class Position:
    __match_args__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y

def vertical(position):
    match position:
        case Position(x=0, y=y):
            return y
        case _:
            return None

The keyword form states which attributes it inspects. With the declared __match_args__, a positional form such as Position(0, y) can express the same relationship. The tuple maps pattern positions to attribute names. It is not a call to the constructor, despite the resemblance to construction syntax.

This mapping has compatibility consequences. Reordering __match_args__ can change what an existing positional pattern means without changing the pattern's spelling. Keyword patterns make their attribute dependencies explicit. A public class that supports positional matching is publishing an additional interpretation of its data, which deserves the same attention as other interfaces consumed by independent code.

PEP 634 specifies automatically generated __match_args__ for named tuples and dataclasses. For dataclasses, the order follows the corresponding generated constructor arguments, including inherited attributes, while fields excluded from initialization do not appear. That convenience reduces declaration work but also ties positional matching to choices made in the class definition.

Matching does not imply an effect-free inspection

The specification identifies attribute access and equality among the operations involved in matching. Instance checks and length queries can also call behavior supplied by objects. Although ordinary implementations often make these operations simple, the language cannot treat every custom object as passive stored data.

PEP 634 deliberately avoids specifying exactly how many times all such methods run. An implementation can cache some information during a match. A custom object whose getters or comparisons rely on a precise call count therefore makes an assumption the matching contract does not provide.

Mapping patterns have a particularly useful restriction. They use the two-argument get method, so required entries must already exist. A defaultdict does not create a missing entry merely because a pattern asks for it. This separates matching existing structure from manufacturing a default value during inspection.

I prefer patterns over repeated checks when the data already has a stable, useful shape. Trees and structured messages meet that description more often than a loose collection of arbitrary objects. The benefit is that the selection condition and the values it establishes remain adjacent in the source.

The grammar also preserves existing uses of match and case as identifiers through soft keywords. Their keyword role depends on context, so adding the construct does not require reserving those names everywhere. PEP 635 explains that match already appears frequently in existing code. This choice limits source incompatibility while retaining names that describe the operation. It also means syntax tools need to recognize the grammatical context rather than highlight every occurrence as a keyword. The compatibility decision affects editors and parsers as well as programs that happen to use either name for a variable.

Diagnostics identify the malformed construct

Python 3.10 also changes the information available when source code is invalid. The versioned release notes describe unclosed delimiters pointing to their opening location. Previously, a parser could report an apparently unrelated later statement where it finally detected that the surrounding expression could not be completed.

The same notes describe full error ranges for expressions responsible for a SyntaxError. A generator expression that requires parentheses can have the relevant expression highlighted, rather than a single position where parsing stopped. Specialized messages cover missing commas and missing block colons. These changes provide a more specific explanation of the rejected grammar.

They matter because a detection location and a correction location can differ. An omitted delimiter changes the interpretation of subsequent lines, so reporting only the last token can send the reader to code that is otherwise valid. Retaining enough parser context to identify the earlier construct reduces that ambiguity without changing the program's semantics.

The release notes also distinguish exception generation from exception display. Suggestions for similar names in AttributeError and NameError depend on PyErr_Display. A custom display function may not show them. A tool that formats exceptions independently cannot assume that upgrading the interpreter automatically reproduces every suggestion visible in the standard display path.

These diagnostic improvements complement the new syntax by making the interpreter's decisions more inspectable. Structural matching adds rules about captures, protocols and case selection that deserve explicit examples. When a program violates the grammar, the release can now report a more useful span and, for several common mistakes, identify the missing syntactic element directly.