Structural Pattern Matching
Reviewed & published by Brayan K
Structural pattern matching is Python's match/case statement (3.10+) that branches on the shape of data — the structure of a list, dict, or object — while pulling out its pieces in the same step.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Think of it as a far smarter if/elif chain: it can destructure sequences, mappings, and class instances, capture values, use a wildcard, and add guard conditions.
What You'll Learn in This Lesson
- match/case basics (Python 3.10+)
- Literal patterns vs capture patterns
- The wildcard _ as a catch-all
- Sequence patterns like [x, y, *rest]
- Mapping patterns and class patterns
- Adding if-guards to a case
1 Literals, Capture, and the Wildcard _
A match tests a value against each case top to bottom. A bare name captures the value; _ is the catch-all default:
def describe(status):
match status:
case 200: # literal pattern
return "OK"
case 404:
return "Not Found"
case 500 | 502 | 503: # OR pattern with |
return "Server Error"
case code: # capture pattern (binds 'code')
return f"Unknown code: {code}"
print(describe(200)) # OK
print(describe(503)) # Server Error
print(describe(418)) # Unknown code: 418# Map HTTP-style commands using literals, OR-patterns, and _
def handle(command):
match command:
case "start":
return "Starting up"
case "stop" | "halt": # OR pattern
return "Shutting down"
case "":
return "Empty command"
case other: # capture anything else
return f"Unknown: {other}"
print(handle("start")) # Starting up
print(handle("halt")) # Shutting down
print(handle("")) # Empty command
print(handle("dance")) # Unknown: dance
# ✅ Expected output:
# Starting up
# Shutting down
# Empty command
# Unknown: dance📦 Sequence Patterns
Patterns can match the structure of a list or tuple and bind its parts — including a *rest to soak up the remainder:
def parse(point):
match point:
case []:
return "empty"
case [x]: # exactly one item
return f"single: {x}"
case [x, y]: # exactly two items
return f"pair: {x}, {y}"
case [first, *rest]: # one or more; *rest gets the tail
return f"first={first}, rest={rest}"
case _:
return "not a list"
print(parse([])) # empty
print(parse([9])) # single: 9
print(parse([1, 2])) # pair: 1, 2
print(parse([1, 2, 3, 4])) # first=1, rest=[2, 3, 4]Sequence patterns match lists and tuples by length and shape. The *rest star works just like unpacking — it captures however many items are left over.
2 Mapping Patterns, Class Patterns & Guards
Match dictionaries by their keys, class instances by their fields, and refine any case with an if guard:
def route(event):
match event:
# Mapping pattern: match by keys, bind the values
case {"type": "click", "x": x, "y": y}:
return f"Click at ({x}, {y})"
case {"type": "key", "code": code}:
return f"Key {code}"
# Guard: an extra if condition on the case
case {"type": "scroll", "delta": d} if d > 0:
return "Scroll up"
case {"type": "scroll"}:
return "Scroll down"
case _:
return "unknown event"
print(route({"type": "click", "x": 10, "y": 20})) # Click at (10, 20)
print(route({"type": "key", "code": "Enter"})) # Key Enter
print(route({"type": "scroll", "delta": 3})) # Scroll up
print(route({"type": "scroll", "delta": -1})) # Scroll downfrom dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def locate(p):
match p:
case Point(x=0, y=0): # class pattern, literal fields
return "origin"
case Point(x=0, y=y): # capture y
return f"on Y axis at {y}"
case Point(x=x, y=0):
return f"on X axis at {x}"
case Point():
return "somewhere else"
print(locate(Point(0, 0))) # origin
print(locate(Point(0, 7))) # on Y axis at 7
print(locate(Point(4, 0))) # on X axis at 4# Mapping patterns with a guard to interpret commands
def run(cmd):
match cmd:
case {"action": "move", "dist": d} if d > 0:
return f"Move forward {d}"
case {"action": "move", "dist": d}:
return f"Move back {-d}"
case {"action": "say", "text": t}:
return f"Saying: {t}"
case _:
return "no-op"
print(run({"action": "move", "dist": 5})) # Move forward 5
print(run({"action": "move", "dist": -3})) # Move back 3
print(run({"action": "say", "text": "hi"})) # Saying: hi
print(run({"action": "spin"})) # no-op
# ✅ Expected output:
# Move forward 5
# Move back 3
# Saying: hi
# no-op🎯 Your Turn: Fill in the Blanks
Replace each ___ to complete the patterns so the output matches.
# 🎯 YOUR TURN: replace each ___ to match the expected output
def classify(value):
match value:
case [___]: # 1. match a 2-element list, bind a and b
return f"pair {a}+{b}={a + b}"
case {"name": ___}: # 2. bind the name value into n
return f"hello {n}"
case ___: # 3. catch-all
return "other"
print(classify([2, 3])) # pair 2+3=5
print(classify({"name": "Ada"})) # hello Ada
print(classify(42)) # other
# Expected output:
# pair 2+3=5
# hello Ada
# othercase [a, b]: # 1. two-element sequence pattern
return f"pair {a}+{b}={a + b}"
case {"name": n}: # 2. mapping pattern binding n
return f"hello {n}"
case _: # 3. wildcard catch-all
return "other"Common Errors & How to Fix Them
❌ Expecting a bare name to compare, not capture
PENDING = "pending"
match status:
case PENDING: # does NOT compare to "pending" - it CAPTURES!✅ A bare name always captures. To compare against a constant, use a dotted name (case Status.PENDING:) or a literal (case "pending":).
❌ Putting the catch-all case first
match x:
case _: # matches everything - later cases never run!
...
case 200:
...✅ Cases run top to bottom. Put case _: (and other broad patterns) last.
❌ Using it on Python 3.9 or older
# SyntaxError: match/case needs Python 3.10+✅ Structural pattern matching arrived in Python 3.10. On older versions, fall back to if/elif and isinstance.
🎯 Mini Challenge: Command Interpreter
Build a tiny interpreter that matches command lists of different shapes.
# Mini Challenge: interpret commands given as lists
def interpret(cmd):
match cmd:
case ["go", direction]:
return f"Going {direction}"
case ["repeat", n, *actions] if n > 0:
return f"Repeat {n}x: {actions}"
case ["quit"]:
return "Bye!"
case []:
return "Nothing to do"
case _:
return "Bad command"
print(interpret(["go", "north"])) # Going north
print(interpret(["repeat", 3, "jump", "spin"])) # Repeat 3x: ['jump', 'spin']
print(interpret(["quit"])) # Bye!
print(interpret([])) # Nothing to do
print(interpret(["fly"])) # Bad command
# Expected output:
# Going north
# Repeat 3x: ['jump', 'spin']
# Bye!
# Nothing to do
# Bad command📚 Keep Learning
Go deeper with the official Python documentation:
- Tutorial — match Statements — a gentle, example-led introduction.
- PEP 636 — Pattern Matching Tutorial — the official deep-dive tutorial with every pattern kind.
- Language Reference — The match Statement — the precise semantics and grammar.
Frequently Asked Questions
Lesson complete — you match on shape now!
You can branch with match/case using literal, capture, and wildcard patterns, destructure sequences and mappings, match class instances, and refine cases with if guards.
Practice quiz
In match/case, what does a bare name pattern like 'case code:' do?
- Compares the subject to a variable named code
- Raises an error because names are not allowed
- Always matches and captures the subject into code
- Only matches if code was defined earlier
Answer: Always matches and captures the subject into code. A bare name is a capture pattern: it always matches and binds the subject to that name.
Which Python version first introduced structural pattern matching (match/case)?
- Python 3.10
- Python 3.8
- Python 3.9
- Python 3.12
Answer: Python 3.10. match/case was added in Python 3.10. Older versions raise a SyntaxError.
Given case 500 | 502 | 503:, what kind of pattern is this?
- A guard pattern
- A bitwise-or computation
- A sequence pattern
- An OR pattern matching any of the listed values
Answer: An OR pattern matching any of the listed values. The | combines alternative patterns, so the case matches 500, 502, or 503.
What does 'case [first, *rest]:' bind when matching [1, 2, 3, 4]?
- first=[1, 2, 3] and rest=4
- first=1 and rest=[2, 3, 4]
- first=1 and rest=2
- It does not match a 4-element list
Answer: first=1 and rest=[2, 3, 4]. The *rest star captures the remaining items, so first=1 and rest=[2, 3, 4].
What is the purpose of 'case _:' in a match statement?
- It is a wildcard catch-all that matches anything not caught earlier
- It matches only None
- It captures the value into a variable named underscore
- It marks the end of the match block
Answer: It is a wildcard catch-all that matches anything not caught earlier. The wildcard _ matches anything and does not bind a name, acting like a default branch.
Where should the catch-all 'case _:' be placed?
- First, so it runs before other cases
- Anywhere, order does not matter
- Last, because cases are tested top to bottom
- It must be the only case
Answer: Last, because cases are tested top to bottom. Cases run top to bottom; a catch-all placed first would prevent later cases from ever running.
Which case matches the dict {"type": "click", "x": 10, "y": 20}?
- case {"type": "key", "code": code}:
- case {"type": "click", "x": x, "y": y}:
- case [type, x, y]:
- case ("click", 10, 20):
Answer: case {"type": "click", "x": x, "y": y}:. A mapping pattern matches by keys and binds the corresponding values, so x=10 and y=20.
What does an if-guard add to a case, e.g. case {"delta": d} if d > 0:?
- It loops the case until d > 0
- It ignores the pattern and just checks the condition
- It makes the case optional
- It only matches when both the pattern matches AND the condition is true
Answer: It only matches when both the pattern matches AND the condition is true. A guard is an extra condition; the case fires only when the pattern matches and the guard is true.
For class patterns, what does 'case Point(x=0, y=0):' require?
- Any Point instance regardless of fields
- A Point whose x and y attributes both equal 0
- A dict with keys x and y
- A list of two zeros
Answer: A Point whose x and y attributes both equal 0. A class pattern matches an instance of Point and checks the given fields against the literals.
To compare a subject against a constant instead of capturing it, you should use:
- A bare lowercase name like case pending:
- A wildcard case _:
- A literal or a dotted name, e.g. case "pending": or case Status.PENDING:
- A *star pattern
Answer: A literal or a dotted name, e.g. case "pending": or case Status.PENDING:. Bare names capture; use a literal or a dotted (qualified) name to compare against a constant.
Continue this course
- Previous: The Walrus Operator (:=)
- Next: Unpacking & Star Expressions — Destructure with a, *rest = ... and splat args with * and **
- Quick reference: Python cheat sheet
Frequently asked questions
What is structural pattern matching?
It's the match/case statement added in Python 3.10. Instead of just comparing equality like a switch, it matches the STRUCTURE of data — the shape of a list, the keys of a dict, the type and fields of an object — and can pull out (bind) pieces in the same step. It's like a much smarter if/elif chain that destructures as it tests.
How is match/case different from a switch statement?
A classic switch only checks one value against constants. Python's match goes far beyond that: it can match sequences ([x, y]), mappings ({'key': v}), class instances (Point(x=0)), bind captured names, use a wildcard _, and add if-guards. It's pattern matching, not just value switching, so it handles structured data a switch can't.
What does the underscore _ mean in a case?
The wildcard _ is the catch-all pattern: 'case _:' matches anything that no earlier case caught, like a default branch. It also matches without binding a name, so you can use _ inside larger patterns to mean 'something is here but I don't care what it is', e.g. case [first, _] ignores the second element.
When should I use match/case instead of if/elif?
Reach for match/case when you're branching on the SHAPE of data — different message types, command formats, JSON-like structures, or class instances you want to destructure. For a couple of simple boolean conditions, a plain if/elif is clearer. match shines when destructuring and type-based branching would otherwise need lots of indexing and isinstance checks.