backend / python core / 34_match_case.md

match/case — structural pattern matching (3.10+)

3 min read source

match/case — structural pattern matching (3.10+)

PEP 634/636. More than just a switch statement: it pattern-matches structure.

Basic syntax

def http_status(code):
    match code:
        case 200 | 201 | 204:
            return "ok"
        case 301 | 302:
            return "redirect"
        case 400 | 404:
            return "client error"
        case 500 | 502 | 503:
            return "server error"
        case _:
            return "unknown"

_ is the wildcard (matches anything, doesn’t bind). Use | for alternatives.

Capture patterns

match point:
    case (0, 0):
        print("origin")
    case (x, 0):
        print(f"on x-axis at {x}")
    case (0, y):
        print(f"on y-axis at {y}")
    case (x, y):
        print(f"({x}, {y})")

A bare name (x, y) captures the value into that name. Constants like 0 test equality.

Important: the capture binds in the enclosing scope. After the match, x and y are still defined.

Class patterns

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

@dataclass
class Circle:
    center: Point
    radius: float

def describe(shape):
    match shape:
        case Point(x=0, y=0):
            return "origin"
        case Point(x=x, y=y):                 # capture by attribute name
            return f"point ({x}, {y})"
        case Circle(center=Point(x=0, y=0), radius=r):
            return f"circle at origin, radius {r}"
        case Circle(center=c, radius=r):
            return f"circle at {c}, radius {r}"

For positional matching, classes need __match_args__:

@dataclass
class Point:
    x: int
    y: int
    __match_args__ = ("x", "y")    # @dataclass auto-generates this

match p:
    case Point(0, 0): ...
    case Point(x, y): ...

@dataclass auto-generates __match_args__ for you.

Mapping patterns

def handle(event):
    match event:
        case {"type": "click", "x": x, "y": y}:
            click(x, y)
        case {"type": "key", "key": k}:
            press(k)
        case {"type": t, **rest}:
            print(f"unknown {t}: {rest}")

Mapping patterns match dicts by required keys. Extra keys are ignored unless **rest captures them.

Sequence patterns

match data:
    case []:
        print("empty")
    case [x]:
        print(f"one element: {x}")
    case [x, y]:
        print(f"two: {x}, {y}")
    case [first, *rest]:
        print(f"head {first}, tail {rest}")
    case [first, *_, last]:
        print(f"first {first}, last {last}")

Works on any sequence (list, tuple), not just lists. Strings are iterable but excluded by design — case [x, y] won’t match "ab".

Guard clauses

match point:
    case Point(x, y) if x == y:
        print("on diagonal")
    case Point(x, y) if x > 0 and y > 0:
        print("first quadrant")
    case Point(x, y):
        print(f"({x}, {y})")

The if clause runs after the structural match — must evaluate to truthy for the case to be selected.

Literal vs name traps

case 0:        # matches int 0
case None:     # matches None
case True:     # matches True
case Point(0): # matches Point with x=0 (positional)
case x:        # captures any value into `x`! Not "the variable named x"

To compare against a variable, use a dotted reference:

THRESHOLD = 100
match value:
    case THRESHOLD:    # captures into THRESHOLD!
    case .THRESHOLD:   # SyntaxError

# Correct: use a class attribute or ENUM
class Config:
    THRESHOLD = 100

match value:
    case Config.THRESHOLD:    # compares equality
        ...

This is a real footgun. Use UPPER constants on a class/module reference, not bare locals.

Real-world example: AST traversal

def evaluate(node):
    match node:
        case {"op": "+", "left": l, "right": r}:
            return evaluate(l) + evaluate(r)
        case {"op": "*", "left": l, "right": r}:
            return evaluate(l) * evaluate(r)
        case {"op": "lit", "value": v}:
            return v
        case _:
            raise ValueError(f"unknown node: {node}")

Pattern matching shines for AST/event/protocol parsing.

Interview angle

  • “What does match/case give you over if/elif?” (Structural deconstruction, exhaustiveness, capture-and-bind, more readable for nested data.)
  • “When does case x: capture vs compare?” (Always captures if x is a bare name. Use dotted access for constants.)
  • “Match a list with at least 2 elements” → case [_, _, *_]:.