Python match-case: branch on the shape of your data

Use Python structural pattern matching for literals, sequences, mappings, and class instances, with guards, capture rules, and explicit fallback behavior.

Best practices
Concurrent tasks moving through a shared processing hub

Structural pattern matching, introduced in Python 3.10, chooses a branch by matching both values and data structure. A case can unpack a sequence, inspect mapping entries, or extract object attributes while binding names for the branch body. It is most useful for events, messages, commands, and syntax trees with distinct shapes.

Use it when the structure makes the decision clearer. A small Boolean condition or dictionary lookup may still be simpler.

Match literal values

def http_status(code):
    match code:
        case 200:
            return "Success"
        case 404:
            return "Not found"
        case 500 | 502 | 503:
            return "Server error"
        case _:
            return "Unknown status"

| combines alternatives; it is not the Boolean or operator. Cases are tried in order, and the first matching case whose guard succeeds runs. There is no switch-style fallthrough into the next case.

_ matches without binding a name. If no case matches and there is no fallback, execution continues after the match statement without raising an error. In a function with no later return, that can mean an implicit None. Literal matching also does not replace type validation: numeric literal patterns use equality, so a value such as 200.0 can match 200.

Match types and extract attributes

from dataclasses import dataclass


@dataclass
class Point:
    x: float
    y: float


def area(shape):
    match shape:
        case bool():
            raise TypeError("A Boolean is not an area input")
        case int() | float() as number:
            return number * number
        case str():
            return "A string has no area"
        case Point(x=0, y=0):
            return 0
        case Point(x=x, y=y):
            return abs(x * y)
        case _:
            raise TypeError("Unsupported input")

This demonstrates several patterns rather than a general geometry API: a number is treated as a square's side, and a Point's coordinates are treated as dimensions. The string branch deliberately returns a message, so the example is not a uniformly numeric production contract.

int() and float() are class patterns, not constructor calls. They match instances of those types, including subclasses. Since bool is an int subclass, the Boolean case appears first to reject it explicitly.

Point(x=x, y=y) checks the class and binds its attributes to local variables. Dataclass annotations do not enforce runtime field types by themselves; validate the object's data separately if arithmetic requires it. See the match statement reference.

Unpack command sequences

This complete example uses simple handlers so you can run it directly:

def move(direction):
    print("Moving", direction)


def pick(item):
    print("Picking up", item)


commands = ["go north", "pick key map", "drop a b c d", "quit"]
for command in commands:
    match command.split():
        case ["quit"]:
            break
        case ["go", direction]:
            move(direction)
        case ["pick", *items]:
            for item in items:
                pick(item)
        case ["drop", *rest] if len(rest) > 3:
            print("Drop at most three items at a time")
        case _:
            print("Unsupported command")

The starred pattern collects remaining elements into a list and can collect zero elements. The if clause is a guard: it runs after the structural pattern matches and its bindings are available. Keep guards readable and avoid unexpected side effects.

This example only rejects an oversized drop command; it does not implement dropping up to three items. Those commands reach the fallback until you add their handler.

Sequence patterns work with supported sequence types, not every iterable. They do not consume arbitrary generators or treat str, bytes, and bytearray as character sequences. The explicit split() is what turns each command string into a list here.

Match mapping entries

def handle_click(x, y):
    return ("click", x, y)


def submit():
    return "submitted"


def dispatch(event):
    match event:
        case {"type": "click", "x": x, "y": y}:
            return handle_click(x, y)
        case {"type": "key", "key": "Enter"}:
            return submit()
        case {"type": str() as event_type}:
            return f"Unhandled event: {event_type}"
        case _:
            return "Invalid event shape"

Mapping patterns are partial: the listed keys and matching values are required, but extra keys are allowed. A click event can include a timestamp without changing this branch. Use **rest to capture additional entries if needed.

The click pattern binds x and y; it does not prove they are valid coordinates. Add class patterns, guards, or an explicit validation layer when values need stronger constraints. Structural dispatch is useful for parsed JSON events, but it is not a schema validator.

Know when another construct is clearer

Use if for a small choice driven by Boolean conditions. Use a mapping and .get() when a value simply selects another value. Complex guards can make a match harder to understand than a direct condition.

Choose match when several shapes need different handling and the variables extracted from each shape belong naturally in that branch. Review case order so a broad pattern does not hide a more specific one.

Common questions

Does a bare name compare with an existing constant?

No. In case RED:, RED is a capture pattern that binds the subject; it is not a lookup of an existing constant. A bare capture without a guard is irrefutable and can make later cases invalid. Use a literal or a dotted value pattern such as Color.RED to compare a named constant. _ is the special wildcard that does not bind.

Does match run faster than if/elif?

There is no universal winner. Choose for clarity first, then benchmark a real hot path with representative inputs if its cost matters.

Can custom classes support positional patterns?

Yes. __match_args__ specifies which attributes correspond to positional pattern arguments. Dataclasses normally generate it for their eligible fields, so the Point above also supports case Point(0, 0). Keyword patterns such as Point(x=0, y=0) avoid depending on positional field order. The PEP 636 tutorial explains these patterns and capture rules.

Diagnose unhandled messages

If a service receives an unknown event shape, log its safe type/category and a request identifier instead of dumping the entire payload. Guance's log collection documentation provides a way to inspect those application records. Validation failures and unsupported event versions can then be distinguished from defects inside a handler.

The examples were run locally, including fallbacks, Boolean rejection, extra mapping keys, and positional dataclass matching. No performance benchmark or Guance ingestion test was performed. Related reading: Start a Python HTTP server (Chinese) and Python scheduling (Chinese).

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting