Structural Pattern Matching in Python

Structural Pattern Matching extends if/elif logic with declarative, data-shape-based matching. It allows code to deconstruct complex data structures and branch based on both type and content. Unlike switch in other languages, pattern matching inspects structure and value, not just equality1.

match command:
    case ("move", x, y):
        handle_move(x, y)
    case ("stop",):
        handle_stop()
    case _:
        print("Unknown command")

Syntax

Basic

match subject:
    case pattern_1 if guard_1:
        ...
    case pattern_2 if guard_2:
        ...
    case _:
        ...
  • The subject is evaluated once.
  • Each case pattern is tested in order.
  • The first pattern that matches (and whose optional if guard succeeds) executes.
  • The _ pattern matches anything (a wildcard).

Pattern Types

Literal

Match exact constants or values:

case 0 | 1 | 2:
    ...
case "quit":
    ...

Multiple literals can be combined with | (OR patterns).

Capture

Assign matched values to variables:

case ("move", x, y):
    # binds x and y

⚠️ Names in patterns always bind, they do not compare. To compare to an existing variable, use a value pattern:

case Point(x, y) if x == origin.x:

Sequence

Match list or tuple structure:

case [x, y, z]:
    ...
case [first, *rest]:
    ...

Mapping

Match dictionaries:

case {"type": "point", "x": x, "y": y}:
    ...

Keys are matched literally; missing keys cause no match.

Class

Deconstruct class instances via their attributes or positional parameters:

case Point(x, y):
    ...

This uses the class’s __match_args__ attribute to define positional fields.

Example:

class Point:
    __match_args__ = ("x", "y")
    def __init__(self, x, y):
        self.x, self.y = x, y

OR

Combine multiple alternatives:

case "quit" | "exit":
    ...

AS

Bind the entire match while destructuring:

case [x, y] as pair:
    ...

Wildcard

The _ pattern matches anything and never binds.

Guards (if clauses)

Optional if conditions refine matches:

match point:
    case Point(x, y) if x == y:
        print("on diagonal")

Guards are evaluated after successful structural match and can use bound names.

Semantics

ConceptBehavior
Evaluationsubject evaluated once; patterns checked in order
BindingSuccessful match creates new local bindings
FailureNon-matching case continues to next pattern
ExhaustivenessNo implicit else; always include case _: for completeness
GuardsBoolean expressions using pattern-bound variables

Examples2

Algebraic Data Types (ADTs)

Pattern matching elegantly models variant data:

class Node: pass
class Leaf(Node): ...
class Branch(Node):
    __match_args__ = ("left", "right")

def depth(tree):
    match tree:
        case Leaf(): return 1
        case Branch(l, r): return 1 + max(depth(l), depth(r))

Command Parsing

def process(cmd):
    match cmd.split():
        case ["load", filename]:
            load_file(filename)
        case ["quit" | "exit"]:
            sys.exit()
        case _:
            print("Unknown command")

HTTP-like Routing

match (method, path):
    case ("GET", "/"):
        return homepage()
    case ("GET", "/users"):
        return list_users()
    case ("POST", "/users", data):
        return create_user(data)

Design3

Goals

  • Provide clarity and conciseness for branching on structured data.
  • Support static analysis: patterns are explicit and compositional.
  • Encourage declarative code, replacing complex if ladders.

Why Not Switch?

  • Structural, not value-only: matches shape, type, and contents.
  • Integrates with Python’s dynamic typing and destructuring capabilities.

Why Not Functions?

While if statements or dispatch tables can emulate simple branching, pattern matching better communicates intent and is easier to read and verify.

Spec

CategoryRule
Subject typesAny object, including sequences, mappings, and classes
Match protocolFor class patterns, Python checks __match_args__ and attributes
Sequence matchRequires __len__ and __getitem__ methods
Mapping matchRequires .keys() and __getitem__; ignores extra keys
Pattern scopeVariables bound within a case are local to that block
Evaluation orderTop-to-bottom, left-to-right
ErrorsSyntaxError for invalid pattern constructs

Pitfalls

  1. Shadowing: Every bare name in a pattern binds, it doesn’t compare:

    color = "red"
    match color:
        case color:  # always matches and binds new variable!
            ...
    

    Use constants or enums instead:

    match color:
        case "red": ...
    
  2. Ignoring guards: Guards run after matching, not during expensive side effects inside guards are discouraged.

  3. Over-matching: Pattern length must align unless *rest is used.

Tooling

  • Linters: flake8, ruff, and pyright support pattern syntax.
  • Static analyzers: Type checkers can verify exhaustive matches on enums and dataclasses.
  • Refactoring tools: can replace nested if trees with match statements.

Usage Patterns

Use CasePattern Example
Enum dispatchcase Status.OK:
Dataclassescase Point(x, y):
Command tuplescase ("move", x, y):
JSON-like dictscase {"user": name, "id": uid}:
Error handlingcase {"error": msg} if "fatal" in msg:

Backwards Compatibility and Evolution

  • Introduced in Python 3.104.
  • Future extensions may include:
    • Better exhaustiveness checking
    • Improved IDE refactoring tools
    • Expanded type integration for dataclasses and typing constructs

Backward-incompatible syntax changes are unlikely; the match semantics are stable.

Summary

Pattern matching provides:

  • Declarative branching over structured data
  • Readable syntax for destructuring and filtering
  • Powerful composition of match conditions and guards

It is not a replacement for if statements. It is a new control structure for expressing shape-based logic cleanly and expressively.