C.W.K.
Stream
Lesson 02 of 07 · published

match Statement — Structural Pattern Matching (3.10+)

~20 min · match, pattern-matching, switch, 3.10

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

Not a switch statement — something stranger and more useful

Python 3.10 added match, and on the surface it looks like a switch from C-family languages. It isn't. It's structural pattern matching — you're not just checking equality, you're describing the shape of the value (a list of three things, a dict with these keys, a class instance with these attributes), and Python destructures the match into named variables for you.

Cases — literal, capture, sequence, mapping, class

Five common patterns. Literal: case 0:, case "x":. Capture: case x: binds anything to x. Sequence: case [a, b, *rest]: destructures a list. Mapping: case {"name": name}: grabs a key. Class: case Point(x=0, y=y): matches a Point with x=0 and binds y. The _ wildcard is the catch-all default.

The OR pattern, the guard, and the AS clause

Patterns can be combined with |: case 1 | 2 | 3:. Guards (if after a pattern) refine when a case matches: case x if x > 0:. The as clause names a sub-pattern: case [Point(x, y) as p, *_]: binds the whole first element to p.

When NOT to use match

If your branches are simple equality checks — case 1: case 2: case 3: — a dict dispatch or an if-chain is just as clean. match earns its weight when you're destructuring a structured value (parsed AST, JSON-shaped data, custom classes). For the C-style switch use case, you didn't need match. For the "take this complex value apart" use case, match is the cleanest tool Python has.

Warning: A bare name in a case pattern is a capture, not a comparison. case foo: matches anything and binds it to foo. To compare against a constant variable, use a dotted name (case Status.ACTIVE:) or wrap it (case (CONST,):). This is the most common confusion.

Code

Basic match — literal and wildcard·python
def describe(status):
    match status:
        case 200:
            return "OK"
        case 301 | 302:
            return "redirect"
        case 404:
            return "not found"
        case n if 500 <= n < 600:
            return "server error"
        case _:
            return "unknown"

print(describe(200))      # OK
print(describe(301))      # redirect
print(describe(503))      # server error
print(describe(700))      # unknown
Sequence patterns — destructuring lists/tuples·python
def parse(command):
    match command.split():
        case ["quit"]:
            return "exit"
        case ["go", direction]:
            return f"moving {direction}"
        case ["go", direction, distance]:
            return f"moving {direction} by {distance}"
        case [action, *args]:
            return f"action={action}, args={args}"
        case []:
            return "empty command"

print(parse("quit"))               # exit
print(parse("go north"))           # moving north
print(parse("go north 5"))         # moving north by 5
print(parse("jump high quickly"))  # action=jump, args=['high', 'quickly']
print(parse(""))                   # empty command
Mapping patterns — JSON-shaped data·python
def handle(event):
    match event:
        case {"type": "login", "user": user}:
            return f"login: {user}"
        case {"type": "logout", "user": user, "reason": reason}:
            return f"logout: {user} ({reason})"
        case {"type": "error", **rest}:
            return f"error event: {rest}"
        case _:
            return "unhandled"

print(handle({"type": "login", "user": "alice"}))
print(handle({"type": "logout", "user": "alice", "reason": "timeout"}))
print(handle({"type": "error", "code": 500, "msg": "oops"}))
Class patterns — destructuring instances·python
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Circle:
    center: Point
    radius: float

def shape_summary(shape):
    match shape:
        case Point(0, 0):
            return "origin"
        case Point(x, 0):
            return f"on x-axis at {x}"
        case Point(x, y):
            return f"point at ({x}, {y})"
        case Circle(Point(0, 0), r):
            return f"circle centered at origin, r={r}"
        case Circle(_, r) if r > 100:
            return f"big circle r={r}"
        case _:
            return "unknown shape"

print(shape_summary(Point(0, 0)))
print(shape_summary(Point(3, 0)))
print(shape_summary(Circle(Point(0, 0), 5)))
The capture vs constant gotcha·python
ACTIVE = "active"
INACTIVE = "inactive"

def status_of(s):
    match s:
        case ACTIVE:           # WRONG — this is a capture, matches ANYTHING
            return "is active"
        case _:
            return "unknown"

print(status_of("foo"))   # 'is active'  <- bug! captured 'foo' as ACTIVE

# RIGHT — use dotted name (e.g., enum or class attribute)
class Status:
    ACTIVE = "active"
    INACTIVE = "inactive"

def status_of_v2(s):
    match s:
        case Status.ACTIVE:    # correct — comparison against constant
            return "is active"
        case _:
            return "unknown"

print(status_of_v2("foo"))    # 'unknown'  ✓
print(status_of_v2("active")) # 'is active'  ✓

External links

Exercise

Write a function process(message) that takes a dict and uses match to dispatch on its type field. Handle three cases: {"type": "chat", "text": ...} returns the text uppercased; {"type": "command", "name": ..., "args": [...]} returns a string showing the command + arg count; {"type": "event", "name": ..., **rest} returns the event name and the count of extra fields. Use a wildcard for unknown shapes that returns "unknown". Test with at least four messages.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.