Polyglot
Languages Python pattern matching
Python § pattern-matching

Pattern matching

Python 3.10 introduced structural pattern matching — the match/case statement (PEP 634, 635, 636). The mechanism admits dispatching on both the value and the structure of an expression: literal matches, sequence patterns, mapping patterns, class patterns, and combinations of these. The form fills a gap between if/elif chains (good for value comparison but verbose for structural checks) and the runtime polymorphism of method dispatch (good for type-driven code but heavy when the structure is the principal axis of variation). Modern Python uses match extensively for sum-type discrimination, parsing, and protocol implementation.

This page covers the pattern grammar, the principal pattern forms, exhaustiveness, and the conventions for using match in idiomatic code.

The match statement

The basic form:

match subject:
    case pattern1:
        body1
    case pattern2 if guard:
        body2
    case _:
        default_body

The subject is evaluated; the patterns are tried in order; the first matching pattern’s body executes. The _ is the wildcard — matches anything; conventional for the catch-all default.

def classify(n):
    match n:
        case 0:
            return "zero"
        case 1:
            return "one"
        case 2 | 3 | 5 | 7 | 11 | 13:
            return "prime"
        case _:
            return "other"

The | admits or-patterns — match any of the listed alternatives.

Pattern forms

The principal patterns:

PatternExampleMatches
Literal42, "x", True, NoneThe exact value
Capturen, nameAnything; binds to the variable
Wildcard_Anything; binds nothing
Or1 | 2 | 3Any of the alternatives
Sequence[1, 2, x], (a, b), []Sequences of matching shape
Mapping{"key": x}, {}Mappings with the named keys
ClassPoint(x=0, y=0)Instances of the class with matching fields
As[1, 2, 3] as tripleMatch and bind the whole
Guardcase n if n > 0:Match plus a boolean condition

Literal patterns

Match against constant values:

match command:
    case "quit":
        exit()
    case "help":
        show_help()
    case "":
        prompt()

The literal patterns admit numbers, strings, True, False, None, and qualified attributes like Color.RED:

from enum import Enum

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

match c:
    case Color.RED:
        return "red"
    case Color.GREEN:
        return "green"
    case Color.BLUE:
        return "blue"

The qualified-name form (Color.RED) is the conventional way to match against an enum value.

Capture patterns

A simple identifier captures the matched value:

match value:
    case n:                     # captures anything; binds to n
        print(f"got {n}")

A capture pattern alone matches anything — equivalent to case _: plus a binding. It is conventionally used inside more elaborate patterns:

match point:
    case (x, y):                # tuple destructuring; x and y are captures
        return f"({x}, {y})"

The _ wildcard does not bind; capture patterns do. The distinction matters when the value is needed in the body.

Or-patterns

Match if any alternative matches:

match status:
    case "ok" | "done" | "complete":
        return Success()
    case "fail" | "error" | "broken":
        return Failure()

Captures inside or-patterns must bind the same names across alternatives:

match request:
    case Move(x, y) | Resize(x, y):       # both bind x and y
        process_coordinates(x, y)

Sequence patterns

Match against sequences:

match items:
    case []:
        return "empty"
    case [x]:
        return f"single: {x}"
    case [x, y]:
        return f"pair: {x}, {y}"
    case [first, *rest]:
        return f"first: {first}, rest: {rest}"
    case [*head, last]:
        return f"head: {head}, last: {last}"
    case [first, *middle, last]:
        return f"first: {first}, middle: {middle}, last: {last}"

The *name admits matching a variable-length subsequence; *_ admits an unnamed subsequence. The pattern works on any sequence — list, tuple, string-like (with care; strings match characters), range.

Mapping patterns

Match against dictionaries:

match data:
    case {}:
        return "empty"
    case {"name": n, "age": a}:
        return f"{n} is {a}"
    case {"error": msg}:
        return f"error: {msg}"
    case {"type": "click", "x": x, "y": y}:
        handle_click(x, y)

The match succeeds if the dict has at least the named keys; extra keys are admitted. To capture remaining items:

match data:
    case {"name": n, **rest}:
        return f"{n} with extra: {rest}"

The **rest admits matching the remaining keys.

The mapping pattern is the conventional Python form for “JSON-like dispatch”.

Class patterns

Match against class instances:

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

match shape:
    case Point(x=0, y=0):
        return "origin"
    case Point(x=0, y=y):
        return f"on y-axis at {y}"
    case Point(x=x, y=0):
        return f"on x-axis at {x}"
    case Point(x=x, y=y):
        return f"({x}, {y})"

The form Point(x=0, y=0) matches if the value is a Point and its x is 0 and its y is 0. The keyword form (x=0) matches against the named attribute.

For positional matching, the class declares __match_args__:

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

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

match shape:
    case Point(0, 0):                # positional; uses __match_args__
        return "origin"
    case Point(x, y):
        return f"({x}, {y})"

For dataclasses, __match_args__ is set automatically:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

match shape:
    case Point(0, 0):
        return "origin"
    case Point(x=0, y=y):            # keyword form also works
        return f"y-axis at {y}"
    case Point(x, y):                 # positional
        return f"({x}, {y})"

The conventional contemporary approach is to use dataclasses; the positional and keyword forms both work without manual __match_args__.

Built-in matches

Several built-ins admit single-positional matching:

match value:
    case bool(True):
        return "true"
    case int(n):
        return f"int: {n}"
    case str(s):
        return f"string: {s}"
    case list(xs):
        return f"list: {xs}"

For built-in types, a single-positional class pattern matches the value and captures it under the given name. The form is the conventional Python idiom for type-and-bind in match.

Guards

A pattern may carry a when-style condition (Python uses if for guards):

match value:
    case n if n > 0:
        return "positive"
    case n if n < 0:
        return "negative"
    case 0:
        return "zero"

The guard is evaluated after the pattern matches; if false, the case is rejected and the next is tried.

Guards admit arbitrary boolean expressions:

match (status, value):
    case ("ok", v) if v > 0:
        return "positive ok"
    case ("ok", _):
        return "non-positive ok"
    case _:
        return "not ok"

As-patterns

Capture the whole value in addition to the destructured parts:

match point:
    case [x, y] as p:
        return f"point {p} with x={x}"

The as admits the whole sequence-pattern’s value to bind to p. The conventional uses are when both the structure and the value are needed.

Recursive patterns

Patterns nest:

match tree:
    case Node(value=v, left=Node(), right=Node()) as full:
        # both children are also Nodes
        return f"full node {v}"
    case Node(value=v, left=None, right=None):
        # leaf
        return f"leaf {v}"
    case Node(value=v):
        return f"partial node {v}"

The pattern admits substantial structural matching — the closest Python comes to algebraic-data-type matching of functional languages.

Common patterns

Tagged dispatch

def handle_event(event: dict) -> None:
    match event:
        case {"type": "click", "x": x, "y": y}:
            on_click(x, y)
        case {"type": "key", "code": code}:
            on_key(code)
        case {"type": "scroll", "delta": delta}:
            on_scroll(delta)
        case {"type": kind, **_}:
            log_unknown(kind, event)

The conventional Python form for “JSON-like discrimination”. match is substantially clearer than the equivalent if/elif cascade.

Sum-type via dataclass

from dataclasses import dataclass

@dataclass
class Click:
    x: int
    y: int

@dataclass
class Key:
    code: int

@dataclass
class Scroll:
    delta: int

Event = Click | Key | Scroll      # type alias

def handle(event: Event) -> None:
    match event:
        case Click(x, y):
            on_click(x, y)
        case Key(code):
            on_key(code)
        case Scroll(delta):
            on_scroll(delta)

The combination of dataclass + Union + match is the conventional Python idiom for algebraic data types.

Result type

from dataclasses import dataclass

@dataclass
class Success[T]:
    value: T

@dataclass
class Failure:
    error: str

type Result[T] = Success[T] | Failure

def compute() -> Result[int]:
    if condition:
        return Success(42)
    return Failure("oops")

def display(r: Result[int]) -> str:
    match r:
        case Success(v):
            return f"value: {v}"
        case Failure(e):
            return f"error: {e}"

The pattern admits typed value-or-error returns.

Tuple destructuring

match point:
    case (0, 0):
        return "origin"
    case (x, 0):
        return f"x={x}"
    case (0, y):
        return f"y={y}"
    case (x, y):
        return f"({x}, {y})"

Tuple destructuring admits dispatch on the shape and contents of a coordinate-like value.

Sequence head and tail

match items:
    case []:
        return "empty"
    case [head, *tail]:
        return f"head={head}, tail={tail}"

The head-and-tail pattern admits recursive list processing similar to Haskell.

Match against literals with capture

match command_args:
    case ("--verbose",):
        verbose = True
    case ("-o", path):
        output = path
    case ("-n", n):
        count = int(n)
    case args:
        log(f"unrecognised args: {args}")

The pattern admits parsing simple command-line arguments.

When match is not the right tool

The conventional contemporary advice:

  • Use match for structural discrimination — when the shape of the value matters.
  • Use if/elif for boolean discrimination — when the conditions are arbitrary expressions.
  • Use polymorphism (subclasses with their own methods) when the operation varies by type and the type set is open.
# Boolean — use if/elif:
if x > 100:
    handle_large(x)
elif x > 10:
    handle_medium(x)
else:
    handle_small(x)

# Structural — use match:
match value:
    case [a, b, *_]:
        ...
    case {"type": t, **kwargs}:
        ...

# Polymorphism — use methods:
shape.area()    # dispatches on the type of shape

The choice depends on the axis of variation; match is the right tool when the structure is the principal axis.

A note on the limits of match

Python’s match is less elaborate than functional-language pattern matching:

  • No exhaustiveness checking — the type checker (mypy, pyright) attempts it but is not always complete.
  • No view patterns — patterns cannot involve arbitrary function calls.
  • Capture is unguarded — a bare name (x) captures rather than matches a constant; named constants must be qualified (Color.RED).
  • No relational patterns — there is no case > 0:; use guards instead.
  • Limited or-pattern composition — captures must align across alternatives.

The combination is nonetheless substantial; modern Python uses match for many of the cases that older code wrote as long if/elif chains or explicit visitor patterns. The discipline of using match where it shines and if elsewhere is part of fluency in contemporary Python.