✦ For everyone, free.

Practical knowledge for real and everyday life

Home

Structural Pattern Matching in Python

Structural Pattern Matching in Python simplifies complex data handling with clear, efficient structure analysis.

Structural pattern matching in Python is a conditional dispatch mechanism that compares a subject value against one or more ordered structural patterns. It may bind components of that value to names, optionally apply an additional guard condition, and executes the suite associated with the first successful case.


Foundations of Structural Pattern Matching in Python

Structural pattern matching is expressed using a match statement with a subject, followed by one or more ordered case clauses. Each case contains a pattern that attempts to match parts of the subject, may bind names to extracted components, and may include an optional guard — a Boolean expression tested after pattern matching succeeds. The first case whose pattern matches and whose guard (if present) evaluates to true is selected for execution, running its associated suite of statements.

  • The subject is the value being matched.
  • Each case consists of a pattern, an optional guard, and a suite of code.
  • Patterns describe the shape or content to match and can bind names to parts of the subject.
  • Guards are extra conditions evaluated after a successful pattern match.
  • Cases are attempted in source order; matching stops at the first successful case.
  • If no pattern matches or all guards fail, and no catch-all case exists, no code runs.

Unlike ordinary Boolean branching (if/elif), structural patterns inspect the shape and structure of data, including sequences, mappings, class instances, and constants, while binding parts of the subject to names. Patterns are not general expressions; their syntax follows pattern-specific rules rather than normal Python evaluation.

Pattern KindTestsBindsRepresentative Example
LiteralSubject equals literal constantNonecase 42:
ValueSubject equals a referenced valueNonecase Color.RED:
CaptureMatches anything, binds to a nameName bound to matched valuecase name:
WildcardMatches anything, no bindingNonecase _:
SequenceMatches sequence shape and elementsSubpattern bindingscase [x, y, *rest]:
MappingMatches mapping keys and valuesSubpattern bindingscase {"key": value}:
ClassMatches class and attributesAttribute bindingscase Point(x, y):
ORMatches if any alternative matchesBindings compatible across alternativescase 0 | 1 | 2:
ASMatches subpattern and binds whole subjectSubpattern bindings + whole subjectcase Point(x, y) as p:
GuardedPattern plus additional Boolean conditionSubpattern bindingscase Point(x, y) if x > 0:

Here is an introductory match example in Python:

def describe(value):
    match value:
        case 0:
            print("Zero")
        case [x, y]:
            print(f"Pair with elements {x} and {y}")
        case {"name": name}:
            print(f"Mapping with name: {name}")
        case _:
            print("Something else")

describe(0)              # Output: Zero
describe([3, 4])         # Output: Pair with elements 3 and 4
describe({"name": "Al"}) # Output: Mapping with name: Al
describe(42)             # Output: Something else
  • The subject is value.
  • Each case tests a pattern: a literal, a sequence, a mapping, or wildcard.
  • The pattern [x, y] binds elements to x and y.
  • The wildcard _ acts as a catch-all fallback.
  • Only the first matching case is executed.

Subject value Cases (ordered) Pattern 1 literal match Pattern 2 bindings + guard Provisional Bindings

This diagram shows a subject flowing into ordered case patterns. Each pattern attempts structural matching, producing provisional bindings. An optional guard validates the match. The first eligible case with a passed guard is selected, and its suite executes.


Structural Pattern Matching in Python

When a match statement executes, the subject expression is evaluated once before any matching begins. Python then progresses through each case clause in order, attempting to match the subject against the pattern in that case.

Cases do not behave like independent fall-through branches; once a successful match (pattern plus optional guard) is found, no further cases are attempted.

A successful match means the pattern structurally corresponds to the subject value, decomposing it as specified. If no pattern matches, and there is no catch-all case (such as a wildcard _), the match statement completes without executing any case suite.

During a successful match, any names declared in the pattern are bound to corresponding parts of the subject. These bindings are new and distinct from any existing variables with the same names; they do not imply equality tests.

Patterns can be nested: a pattern may contain subordinate patterns that must jointly satisfy the structural form of the matched part. For example, a sequence pattern may contain capture patterns or nested mappings.

Example demonstrating multiple patterns, nested patterns, name binding, and catch-all:

def analyze(value):
    match value:
        case [x, y]:
            print(f"Two-item sequence: {x=}, {y=}")
        case {"type": "point", "coords": [lat, lon]}:
            print(f"Point with latitude={lat} and longitude={lon}")
        case (a, b, c):
            print(f"Triple tuple: {a}, {b}, {c}")
        case _:
            print("No pattern matched")

analyze([10, 20])                          # Two-item sequence: x=10, y=20
analyze({"type": "point", "coords": [1, 2]})  # Point with latitude=1 and longitude=2
analyze((1, 2, 3))                         # Triple tuple: 1, 2, 3
analyze("hello")                           # No pattern matched

Structural pattern matching expresses intent clearly when dispatching on heterogeneous structured data with varying shapes and nested components. For simple equality tests or straightforward lookups, a direct if condition or dictionary lookup may be simpler and clearer.


Literal Patterns in Python

Literal patterns succeed when the subject component compares equal to a supported literal value, such as a numeric constant, string, Boolean, or a null-like singleton such as None.

Singleton constants like None, True, and False receive special identity-oriented treatment: the pattern matches only if the subject is exactly that singleton object, not merely an equal value.

Example:

def check_literal(value):
    match value:
        case "hello":
            print("Greeting detected")
        case 42:
            print("The answer")
        case True:
            print("Boolean true")
        case None:
            print("No value")
        case _:
            print("Something else")

check_literal("hello")  # Greeting detected
check_literal(42)       # The answer
check_literal(True)     # Boolean true
check_literal(None)     # No value
check_literal(False)    # Something else

A literal pattern expresses a constant match condition; it does not assign or create a variable.


Value Patterns in Python

Value patterns compare the subject component against an existing referenced value rather than capturing the subject into a new name.

An unqualified bare name in a pattern normally acts as a capture pattern, binding the matched value to that name. To express a value pattern, you must use a qualified name (e.g., Enum.CONSTANT) to refer to an existing constant.

Example:

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

def describe_color(value):
    match value:
        case Color.RED:
            print("Red color")
        case Color.GREEN:
            print("Green color")
        case Color.BLUE:
            print("Blue color")
        case other:
            print(f"Captured color: {other}")

describe_color(Color.RED)    # Red color
describe_color(Color.GREEN)  # Green color
describe_color(Color.BLUE)   # Blue color
describe_color("yellow")     # Captured color: yellow

Value patterns compare subject components to the referenced values using equality appropriate to the type, but detailed equality semantics are not expanded here.


Capture and Wildcard Patterns in Python

Capture patterns are irrefutable: they match any subject component and bind it to a name without additional constraints.

The wildcard pattern _ matches anything but intentionally discards the matched value without creating a binding.

Capture patterns and wildcards can be used to create catch-all cases. Because capture patterns match anything, placing them early makes subsequent cases unreachable.

Example:

def test_capture(value):
    match value:
        case x:
            print(f"Captured: {x}")
        case _:
            print("Wildcard fallback")  # Unreachable

test_capture(10)  # Captured: 10

Using nested captures and a wildcard fallback:

def test_nested(value):
    match value:
        case [x, y]:
            print(f"Captured pair: {x}, {y}")
        case _:
            print("No match")

test_nested([1, 2])    # Captured pair: 1, 2
test_nested(42)        # No match

A capture name with the same spelling as an existing variable does not test equality; it binds the matched value anew.


Sequence Patterns in Python

Sequence patterns match sequence-like subjects by element position and nested element patterns.

Fixed-length sequence patterns require the subject to have exactly the same number of elements, with each positional subpattern matching the corresponding element.

Starred sequence patterns use a starred pattern (e.g., *rest) to capture or discard a variable-length portion of the sequence, preserving constraints imposed by remaining positional subpatterns.

Nested sequence patterns allow structured extraction, e.g., a coordinate pair inside a larger sequence.

Example:

def parse_sequence(value):
    match value:
        case [x, y]:
            print(f"Pair: {x}, {y}")
        case [head, *middle, tail]:
            print(f"Sequence with head={head}, tail={tail}, middle={middle}")
        case [x, [y, z]]:
            print(f"Nested sequence: x={x}, y={y}, z={z}")
        case _:
            print("No match")

parse_sequence([1, 2])                    # Pair: 1, 2
parse_sequence([1, 2, 3, 4])              # Sequence with head=1, tail=4, middle=[2, 3]
parse_sequence([10, [20, 30]])            # Nested sequence: x=10, y=20, z=30
parse_sequence("not a sequence")          # No match

Text strings and byte strings must not be assumed to behave as ordinary sequence patterns, even though they support indexing and iteration.

Example parsing command tuples:

def command_parser(cmd):
    match cmd:
        case ("move", x, y):
            print(f"Move to {x}, {y}")
        case ("say", message):
            print(f"Say: {message}")
        case ("wait",):
            print("Waiting")
        case _:
            print("Unknown command")

command_parser(("move", 10, 20))   # Move to 10, 20
command_parser(("say", "hi"))      # Say: hi
command_parser(("wait",))          # Waiting
command_parser(("jump",))          # Unknown command

Mapping Patterns in Python

Mapping patterns match mapping-like subjects by requiring specified keys and recursively matching their associated values. The subject mapping may contain additional keys beyond those specified.

Keys in mapping patterns can be literals or value patterns; associated values are matched by nested patterns.

Some Python versions support double-star (**rest) mapping patterns to capture additional unspecified keys.

Example:

def process_record(record):
    match record:
        case {"name": name, "age": age}:
            print(f"Name: {name}, Age: {age}")
        case {"type": "event", "payload": {"id": event_id, **rest}}:
            print(f"Event {event_id} with extra data {rest}")
        case _:
            print("Unknown record")

process_record({"name": "Alice", "age": 30})  # Name: Alice, Age: 30
process_record({"type": "event", "payload": {"id": 42, "extra": True}})  # Event 42 with extra data {'extra': True}
process_record({"foo": "bar"})  # Unknown record

Failure occurs if a required key is missing. Mapping pattern matching is structural, not dictionary equality or simple subscription.

Example processing heterogeneous event dictionaries:

def handle_event(event):
    match event:
        case {"event": "click", "x": x, "y": y}:
            print(f"Click at {x}, {y}")
        case {"event": "keypress", "key": key}:
            print(f"Key pressed: {key}")
        case _:
            print("Unknown event")

handle_event({"event": "click", "x": 10, "y": 20})      # Click at 10, 20
handle_event({"event": "keypress", "key": "Enter"})    # Key pressed: Enter
handle_event({"event": "resize", "width": 100})        # Unknown event

Class Patterns in Python

Class patterns match a subject if it is an instance of a specified class (or subclass) and if selected attributes satisfy subpatterns.

Keyword class patterns explicitly match named attributes, often more clearly communicating the inspected structure than positional patterns.

Positional class patterns match attributes by position according to the class's __match_args__ tuple, which defines the order of attributes to match.

Example class with positional and keyword pattern support:

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

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

def describe_point(obj):
    match obj:
        case Point(x, y):
            print(f"Positional: x={x}, y={y}")
        case Point(x=0, y=y):
            print(f"Keyword with x=0, y={y}")
        case _:
            print("Not a Point")

p = Point(1, 2)
describe_point(p)           # Positional: x=1, y=2
describe_point(Point(0, 5)) # Positional: x=0, y=5

Nested class patterns occur when attributes themselves are matched with other patterns.

Example nested class pattern:

class Rectangle:
    __match_args__ = ("top_left", "bottom_right")

    def __init__(self, top_left, bottom_right):
        self.top_left = top_left
        self.bottom_right = bottom_right

def describe_rectangle(rect):
    match rect:
        case Rectangle(Point(x1, y1), Point(x2, y2)):
            print(f"Rectangle from ({x1}, {y1}) to ({x2}, {y2})")
        case _:
            print("Not a Rectangle")

r = Rectangle(Point(0, 1), Point(2, 3))
describe_rectangle(r)  # Rectangle from (0, 1) to (2, 3)

Class patterns combine class compatibility checks, attribute inspection, nested matching, and bindings. They are more than simple type switches.

Pattern KindStructural BasisComponent SelectionExtra Data HandlingRepresentative CapturesCommon Misconception
SequencePositional elementsBy indexNo extra elements allowedPositional subpattern bindingsAssumes arbitrary iterables can match
MappingKey-value pairsBy specified keysIgnores or captures extra keysValue subpattern bindingsAssumed to require exact key set
ClassClass and attributesBy __match_args__ or keywordsExtra attributes ignoredAttribute subpattern bindingsBehaves like a simple type switch

Pattern Composition in Python

Patterns can be composed by nesting patterns, combining alternatives with OR, capturing values, and binding entire matched values with AS.

  • OR patterns (|) match if any alternative matches. Alternatives must have compatible bindings.

  • AS patterns (as) bind the entire matched value in addition to matching a subpattern.

Example OR pattern combining literals and sequences:

def check_value(value):
    match value:
        case 0 | 1 | 2:
            print("Small integer")
        case [x, y] | (x, y):
            print(f"Pair sequence or tuple: {x}, {y}")
        case _:
            print("Other")

check_value(1)      # Small integer
check_value([3, 4]) # Pair sequence or tuple: 3, 4
check_value((5, 6)) # Pair sequence or tuple: 5, 6
check_value(10)     # Other

Example AS pattern:

def identify_point(value):
    match value:
        case Point(x, y) as p:
            print(f"Point at ({x}, {y}), full object: {p}")
        case _:
            print("Not a point")

Nested composition allows combining any pattern kinds:

def complex_match(value):
    match value:
        case {"pos": (x, y), "color": color} as full:
            print(f"Position: ({x}, {y}), color: {color}, full: {full}")
        case _:
            print("No match")

complex_match({"pos": (1, 2), "color": "red"})

Deeply nested or heavily alternative patterns can reduce readability. In such cases, decomposing logic into smaller patterns or helper functions is advisable.


Match Guards in Python

A match guard is an if condition added after a pattern that is evaluated only if the pattern structurally succeeds and its bindings are available.

If the guard evaluates to False, the case is not selected, and matching continues with subsequent cases.

Example:

def check_point(value):
    match value:
        case Point(x, y) if x == y:
            print("Point on diagonal")
        case Point(x, y):
            print("Point not on diagonal")
        case _:
            print("Not a point")

check_point(Point(1, 1))  # Point on diagonal
check_point(Point(1, 2))  # Point not on diagonal

Guards are suited for expressing additional relational or computational conditions that do not fit naturally inside a structural pattern.

Guards are ordinary expressions evaluated during matching and should remain readable and side-effect free.

Example of an overly complicated guard:

def complicated(value):
    match value:
        case Point(x, y) if (x > 0 and (y < 0 or (x + y) % 2 == 0)):
            print("Complex condition met")
        case _:
            print("No match")

Refactoring the above into clearer structural and guard separation improves readability.


Matching Semantics and Case Design

Cases should be ordered from more specific to more general patterns to avoid accidental shadowing by irrefutable or broad patterns.

Fallback strategies include catch-all cases (e.g., wildcard), explicit unsupported-value handling, or omitting fallback when intentionally doing nothing.

Structural pattern matching is best for expressing stable shape and dispatch relationships, not merely replacing every short equality test or dictionary lookup.

Example contrasting dispatch implementations:

# Using if/elif
def dispatch_if(value):
    if value == 0:
        return "Zero"
    elif isinstance(value, list) and len(value) == 2:
        return f"Pair: {value[0]}, {value[1]}"
    else:
        return "Other"

# Using dictionary lookup
def dispatch_dict(value):
    mapping = {0: "Zero"}
    return mapping.get(value, "Other")

# Using match
def dispatch_match(value):
    match value:
        case 0:
            return "Zero"
        case [x, y]:
            return f"Pair: {x}, {y}"
        case _:
            return "Other"

Common mistakes include:

  • Mistaking capture patterns for value matching.
  • Placing wildcard or capture patterns too early, making later cases unreachable.
  • Assuming sequence patterns match arbitrary iterables.
  • Expecting mapping patterns to reject extra keys.
  • Treating class patterns as automatic destructuring of every attribute.

Example with incorrect and corrected patterns:

def test_patterns(value):
    match value:
        # Incorrect: captures instead of matching literal
        case x if x == 42:
            print("Matched 42 with capture and guard")

        # Correct: literal pattern
        case 42:
            print("Matched literal 42")

        # Incorrect: wildcard placed first, shadows later cases
        case _:
            print("Wildcard catch-all")
        
        # Unreachable:
        case 0:
            print("Zero")  # unreachable

# Corrected order and pattern usage:
def test_patterns_fixed(value):
    match value:
        case 42:
            print("Matched literal 42")
        case 0:
            print("Zero")
        case _:
            print("Wildcard catch-all")

Solved Structural Pattern Matching Exercises in Python

Exercise 1: Command Processing with Literals, Sequences, OR patterns, Capture, Wildcard, and Guard

def process_command(cmd):
    match cmd:
        case ("add", x, y) | ("sum", x, y) if isinstance(x, int) and isinstance(y, int):
            return x + y
        case ("concat", *strings) if all(isinstance(s, str) for s in strings):
            return "".join(strings)
        case ("print", message):
            print(message)
            return None
        case _:
            raise ValueError(f"Unsupported command: {cmd}")

print(process_command(("add", 1, 2)))           # 3
print(process_command(("sum", 10, 5)))           # 15
print(process_command(("concat", "hello", " ", "world")))  # "hello world"
process_command(("print", "Hello!"))             # prints "Hello!"
# process_command(("unknown",))                   # raises ValueError

Explanation:

  • The subject cmd is evaluated once.
  • Cases are ordered to first match "add" or "sum" commands with integer arguments using an OR pattern and guard to ensure types.
  • The "concat" command uses a starred capture *strings and a guard to verify all are strings.
  • The "print" command matches a tuple with a message to print.
  • The catch-all case raises an error for unsupported commands.

Exercise 2: Matching Heterogeneous Mapping-Based Records with Nested Patterns and Extra Key Capture

def process_record(record):
    match record:
        case {"type": "person", "name": name, "age": age, **extras}:
            print(f"Person: {name}, Age: {age}, Extras: {extras}")
        case {"type": "event", "event": event_type, "payload": {"id": id, **details}}:
            print(f"Event {event_type} with ID {id} and details {details}")
        case _:
            print("Unknown record type")

process_record({"type": "person", "name": "Alice", "age": 30, "city": "NY"})
# Person: Alice, Age: 30, Extras: {'city': 'NY'}

process_record({"type": "event", "event": "login", "payload": {"id": 123, "status": "success"}})
# Event login with ID 123 and details {'status': 'success'}

process_record({"foo": "bar"})
# Unknown record type

Explanation:

  • The subject record is matched against mapping patterns requiring specific keys.
  • The "person" case captures name and age, plus any extra keys in extras.
  • The "event" case matches nested mappings for payload, capturing id and other details.
  • The fallback case handles unknown records.
  • This structural matching differs from dictionary equality or simple key lookups by recursively matching nested structures and capturing relevant parts.

Exercise 3: Class Patterns with Positional and Keyword Attributes, Nested Patterns, AS Pattern, and Guard

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

class Circle:
    __match_args__ = ("center", "radius")
    def __init__(self, center, radius):
        self.center = center
        self.radius = radius

def describe_shape(shape):
    match shape:
        case Circle(Point(x, y) as center, r) if r > 0:
            print(f"Circle with center at ({x}, {y}) and radius {r}")
        case Point(x=0, y=0):
            print("Point at the origin")
        case Point(x, y):
            print(f"Point at ({x}, {y})")
        case _:
            print("Unknown shape")

describe_shape(Circle(Point(1, 2), 5))  # Circle with center at (1, 2) and radius 5
describe_shape(Point(0, 0))             # Point at the origin
describe_shape(Point(3, 4))             # Point at (3, 4)
describe_shape("not a shape")           # Unknown shape

Explanation:

  • The subject is matched against class patterns.
  • Circle pattern matches positionally: center is matched as a nested Point pattern with an AS binding center.
  • A guard ensures radius r is positive.
  • Keyword patterns match Point attributes explicitly.
  • The fallback handles unknown shapes.

Reviewing pattern-matching solutions involves checking for:

  • Unnecessary overlapping cases.
  • Overly broad capture patterns shadowing later cases.
  • Inconsistent bindings across alternatives.
  • Excessive nesting that reduces readability.
  • Duplicated guards that could be factored.
  • Missing fallback cases.
  • Patterns encoding incidental structure rather than meaningful domain shape.

Careful design improves clarity, correctness, and maintainability of structural pattern matching logic.