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
subjectis evaluated once. - Each
casepattern is tested in order. - The first pattern that matches (and whose optional
if guardsucceeds) 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
| Concept | Behavior |
|---|---|
| Evaluation | subject evaluated once; patterns checked in order |
| Binding | Successful match creates new local bindings |
| Failure | Non-matching case continues to next pattern |
| Exhaustiveness | No implicit else; always include case _: for completeness |
| Guards | Boolean 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
ifladders.
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
| Category | Rule |
|---|---|
| Subject types | Any object, including sequences, mappings, and classes |
| Match protocol | For class patterns, Python checks __match_args__ and attributes |
| Sequence match | Requires __len__ and __getitem__ methods |
| Mapping match | Requires .keys() and __getitem__; ignores extra keys |
| Pattern scope | Variables bound within a case are local to that block |
| Evaluation order | Top-to-bottom, left-to-right |
| Errors | SyntaxError for invalid pattern constructs |
Pitfalls
-
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": ... -
Ignoring guards: Guards run after matching, not during expensive side effects inside guards are discouraged.
-
Over-matching: Pattern length must align unless
*restis used.
Tooling
- Linters:
flake8,ruff, andpyrightsupport pattern syntax. - Static analyzers: Type checkers can verify exhaustive matches on enums and dataclasses.
- Refactoring tools: can replace nested
iftrees withmatchstatements.
Usage Patterns
| Use Case | Pattern Example |
|---|---|
| Enum dispatch | case Status.OK: |
| Dataclasses | case Point(x, y): |
| Command tuples | case ("move", x, y): |
| JSON-like dicts | case {"user": name, "id": uid}: |
| Error handling | case {"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
typingconstructs
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.