Python

Structural pattern matching — match/case beyond a simple switch

match/case looks like a switch statement borrowed from another language, but its real power is matching the SHAPE of a value — destructuring a list, a dict, or a class's fields as part of the match itself — not just comparing it against a list of exact values.

Intermediate

3 min read

The shallow reading: match as a cleaner if/elif chain

def describe(status_code):
    match status_code:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500:
            return "Server Error"
        case _:
            return "Unknown"  # _ is the WILDCARD — matches anything not caught above

At its simplest, match/case compares a value against a series of patterns, falling through to case _ (the wildcard, matching anything) if nothing else matched — functionally similar to a long if/elif/else chain comparing status_code against each value. This shallow reading is accurate but doesn't show what actually makes match a distinct, more powerful language feature, not just alternate syntax for what if/elif already does.

The real feature: matching structure, and binding names to the pieces that match

def handle_response(response):
    match response:
        case {"status": 200, "data": data}:
            return f"Success: {data}"                 # matches a dict with a status:200 key — data is BOUND from the match
        case {"status": code, "error": message} if code >= 400:  # a GUARD — extra condition beyond the shape
            return f"Error {code}: {message}"
        case {"status": code}:
            return f"Unhandled status {code}"
        case _:
            return "Not a recognized response shape"

Each case here isn't just comparing response against a literal value — it's checking whether response has a particular shape (a dict containing specific keys), and if it matches, binding the corresponding values to new local names (data, code, message) directly as part of the match, with no separate response["data"] lookup needed afterward. The if code >= 400 on the second case is a guard — an additional condition checked only once the structural pattern itself has already matched, letting a case combine "does this have the right shape" with "and does it also satisfy this condition."

Matching sequences: destructuring a list's shape directly in the pattern

def process(command):
    match command.split():
        case ["go", direction]:
            return f"Moving {direction}"
        case ["take", *items]:              # * captures the REST as a list, exactly like unpacking
            return f"Taking: {', '.join(items)}"
        case ["look"]:
            return "Looking around"
        case _:
            return "Unknown command"
 
process("take sword shield potion")  # "Taking: sword, shield, potion"

Sequence patterns destructure a list (or tuple) by both length and position — ["go", direction] only matches a two-element list starting with "go", binding the second element to direction, while ["take", *items] matches any length starting with "take", collecting everything after it into items — the same * unpacking syntax from the earlier *args/**kwargs lesson, applied here as a matching tool rather than a function-call mechanism.

Matching against a class's own structure

from dataclasses import dataclass
 
@dataclass
class Point:
    x: int
    y: int
 
def classify(point):
    match point:
        case Point(x=0, y=0):
            return "origin"
        case Point(x=0, y=y):
            return f"on the y-axis at {y}"
        case Point(x=x, y=0):
            return f"on the x-axis at {x}"
        case Point(x=x, y=y):
            return f"at ({x}, {y})"

match can check both an object's type (Point(...), only matching actual Point instances) and its individual field values or bindings, in one pattern — Point(x=0, y=0) matches only a Point whose x and y are both literally 0, while Point(x=0, y=y) matches any Point with x == 0, binding whatever y actually is. This is a real, structural check against the dataclass's own fields (from the previous lesson), not string comparison or manual attribute access written out by hand.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What does match/case do beyond what an if/elif chain already does?

2. What does a guard clause (the `if` after a case pattern) actually do?

3. How does `case Point(x=0, y=y):` differ from `case Point(x=0, y=0):`?