Backend / Python core / 34_match_case.md

match/case — structural pattern matching (3.10+)

Updated 3 min read source
On this page9
  1. Basic syntax
  2. Capture patterns
  3. Class patterns
  4. Mapping patterns
  5. Sequence patterns
  6. Guard clauses
  7. Literal vs name traps
  8. Real-world example: AST traversal
  9. Interview angle

match/case — structural pattern matching (3.10+)

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

Basic syntax

python
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

python
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

python
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"
        # capture by attribute name
        case Point(x=x, y=y):
            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__:

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

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

@dataclass auto-generates __match_args__ for you.

Mapping patterns

python
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

python
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

python
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

python
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:

python
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

python
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 3

  • “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 [_, _, *_]:.