What you'll learn
- Use
match/caseto branch on the shape of data, not only its value - Destructure sequences, mappings, and class instances in a pattern, binding names as you go
- Add guards with
if, alternatives with|, and a wildcard_ - Know when
matchbeats anif/elifchain and when it does not
Concept
match is not a switch statement, although it can act as one. It compares a value against patterns, and a pattern can describe shape: a tuple of two items, a dictionary with certain keys, an instance of a class with certain attribute values. When a pattern matches, the names inside it are bound to the matching pieces, so testing and unpacking happen in one step. It arrived in Python 3.10 and is the natural way to handle commands, events, JSON-like messages, and dataclasses that come in several forms.

Patterns come in a few kinds and compose. A literal pattern, case 404: or case "quit":, matches by equality. A capture pattern, a bare name, matches anything and binds it. A sequence pattern, case [x, y]:, matches a list or tuple of that length, and *rest collects the remainder. A mapping pattern, case {"type": "click", "pos": pos}:, matches a dictionary that has at least those keys. A class pattern, case Point(x=0, y=y):, matches an instance and its attributes. _ matches anything without binding it, and belongs last as the catch-all.
| Pattern | Example | Matches when |
|---|---|---|
| literal | case 0: / case "ok": |
equal to the value |
| capture | case name: |
anything; binds name |
| sequence | case [a, b, *rest]: |
list/tuple with ≥ 2 items |
| mapping | case {"id": id_, **extra}: |
dictionary with an "id" key |
| class | case Money(amount=0): |
instance with matching attributes |
| or | case "y" | "yes": |
either alternative |
| guard | case [x, y] if x == y: |
pattern matches and the condition holds |
| wildcard | case _: |
anything, binds nothing |
A guard adds a condition after the pattern, evaluated only if the shape matched. Alternatives with | let one case handle several literals or shapes. Enum members and dotted names like Status.PAID are treated as values to compare against, not names to bind, because they contain a dot; a bare name is always a capture, which is the one rule that surprises people. Dataclasses support class patterns by position as well as by keyword, because the decorator records the field order.
Reach for match when you are dispatching on the structure of data: parsing commands, handling messages with a type field, walking a tree of nodes, or handling several dataclass variants. Do not reach for it to replace a simple if x == 1 or a dictionary lookup; those stay clearer as they are. The test is whether the cases destructure something. If every case is a bare literal, a dictionary or an if chain is usually shorter.

Code
A command parser: sequence patterns with literals, captures, and a star, plus | alternatives and the wildcard. Each command becomes readable in one line.
# commands.py
def handle(command):
match command.split():
case ["go", direction]:
return f"walking {direction}"
case ["take", *items] if items:
return f"taking {', '.join(items)}"
case ["look"] | ["l"]:
return "you see a room"
case ["quit" | "exit"]:
return "bye"
case []:
return "say something"
case _:
return f"unknown: {command!r}"
for text in ["go north", "take lamp key", "l", "exit", "", "dance wildly"]:
print(f"{text!r:<18} -> {handle(text)}")
'go north' -> walking north
'take lamp key' -> taking lamp, key
'l' -> you see a room
'exit' -> bye
'' -> say something
'dance wildly' -> unknown: 'dance wildly'Mapping patterns for JSON-like events. Extra keys are allowed, **rest collects them, and a nested pattern reaches inside.
# events.py
events = [
{"type": "click", "pos": {"x": 10, "y": 20}, "button": "left"},
{"type": "key", "key": "Enter"},
{"type": "click", "pos": {"x": 0, "y": 0}},
{"type": "scroll", "delta": -3, "shift": True},
]
for event in events:
match event:
case {"type": "click", "pos": {"x": 0, "y": 0}}:
print("click at origin")
case {"type": "click", "pos": {"x": x, "y": y}, **rest}:
print(f"click at ({x}, {y}) extras={sorted(rest)}")
case {"type": "key", "key": key}:
print(f"key {key}")
case {"type": kind}:
print(f"other event: {kind}")
click at (10, 20) extras=['button']
key Enter
click at origin
other event: scrollClass patterns over dataclasses and an enum, with guards. Dotted names like Status.PAID compare; bare names bind.
# shapes_match.py
from dataclasses import dataclass
from enum import Enum
class Status(Enum):
PAID = "paid"
REFUNDED = "refunded"
@dataclass
class Payment:
amount: float
status: Status
@dataclass
class Point:
x: float
y: float
def describe(obj):
match obj:
case Point(x=0, y=0):
return "origin"
case Point(x=0, y=y) | Point(x=y, y=0):
return f"on an axis at {y}"
case Point(x, y) if x == y:
return f"on the diagonal at {x}"
case Point():
return "somewhere else"
case Payment(amount=a, status=Status.PAID) if a > 100:
return f"large payment of {a}"
case Payment(status=Status.REFUNDED):
return "refunded"
case Payment():
return "small payment"
case _:
return "not a shape or payment"
things = [
Point(0, 0), Point(0, 5), Point(3, 3), Point(1, 2),
Payment(250, Status.PAID), Payment(5, Status.PAID), Payment(5, Status.REFUNDED), "x",
]
for thing in things:
print(describe(thing))
origin
on an axis at 5
on the diagonal at 3
somewhere else
large payment of 250
small payment
refunded
not a shape or paymentWalking a tree: nested tuples as an expression, evaluated with recursive match. This is the kind of code where match is clearly better than the alternative.
# expr_tree.py
def evaluate(expr):
match expr:
case int() | float():
return expr
case ("add", left, right):
return evaluate(left) + evaluate(right)
case ("mul", left, right):
return evaluate(left) * evaluate(right)
case ("neg", inner):
return -evaluate(inner)
case _:
raise ValueError(f"bad expression: {expr!r}")
tree = ("add", ("mul", 3, 4), ("neg", ("add", 1, 1)))
print(evaluate(tree))
print(evaluate(("mul", 2.5, ("add", 1, 1))))
try:
evaluate(("pow", 2, 3))
except ValueError as err:
print(err)
10
5.0
bad expression: ('pow', 2, 3)Try it yourself
Create router.py with a route(request) function where a request is a tuple like ("GET", ["users", "42"]). Use match to return "list users" for ("GET", ["users"]), "user 42" for a GET with an id, "create user" for a POST to users, "not found" for anything else, and "method not allowed" for a DELETE to any path using a guard. Then write an Event dataclass hierarchy of two or three variants and a handler that matches on them.
Common mistakes
- A bare name where you meant a constant.
case STATUS_OK:binds anything toSTATUS_OK; use a dotted name likeCodes.OKor a literal. - Forgetting the order matters. The first matching case wins; put specific patterns before general ones and
case _last. - Using
matchas a switch on plain values. If no case destructures anything, anifchain or a dictionary is simpler.
What's next
matchcompares a value against patterns that describe shape, binding names as it matches.- Literal, capture, sequence, mapping, class,
|, guards, and_compose; dotted names compare, bare names bind. - Use it when cases take data apart: commands, events, dataclass variants, trees.
You can dispatch on structure. Next you will add the type information that makes both readers and tools understand that structure: type hints.