Structural Pattern Matching - Engineering the Shape of Data
Reading time: ~22 minutes | Level: Foundation → Engineering
What does this print?
command = {"action": "move", "direction": "north", "steps": 3}
match command:
case {"action": "move", "direction": direction, "steps": n} if n > 0:
print(f"Moving {direction} by {n}")
case {"action": "move"}:
print("Invalid move command")
case {"action": action}:
print(f"Unknown action: {action}")
case _:
print("Malformed command")
Most developers who have never used match/case will reach for isinstance checks, multiple .get() calls, and a chain of if/elif blocks to do what this does in four lines. The output is Moving north by 3. But what makes this genuinely powerful - and what separates it from a switch statement - is the word structural. The match statement does not compare values. It compares the shape of data.
This lesson teaches you how structural pattern matching works at the implementation level, when it dramatically simplifies real code, and what traps catch engineers who treat it like a C-style switch.
What You Will Learn
- Why
match/caseis fundamentally different from a switch statement - it matches structure, not values - How each pattern type works: literal, capture, wildcard, OR, sequence, mapping, class, and guard
- How CPython 3.10+ optimises certain match statements with a jump table
- The most dangerous pitfall: why
case x:captures everything instead of comparing tox - Real-world applications: CLI command dispatch, JSON API response parsing, AST processing
- When
matchwins overif/elifand whenif/elifis still the right tool - Python version constraints and what to do when targeting Python 3.9 or below
- Six interview questions with detailed answers on pattern matching semantics
Prerequisites
- Python
if/elif/elseand truthiness (Lesson 01 of this module) - Python data structures: lists, tuples, dictionaries (Module 2)
- Basic class definitions and attributes (Module 3)
- Python 3.10 or higher installed (verify with
python --version)
The Mental Model: Structure-Based Dispatch
Before looking at syntax, understand the core idea. An if/elif chain evaluates boolean expressions. Each condition produces True or False. A match statement evaluates patterns - descriptions of the shape an object should have. If the subject fits the shape, the branch fires.
if/elif - Value-Based Dispatch | match/case - Structure-Based Dispatch | |
|---|---|---|
| Mechanism | Evaluates boolean expressions (True/False) | Matches structural patterns (fits/doesn't fit) |
| Binding | Nothing - you access variables manually | Extracts and binds sub-parts of matched data |
| Best for | Arbitrary boolean algebra, range checks | Dispatching on data shape, destructuring |
| Catch-all | else: | case _: (wildcard) |
The structural model is common in functional languages (Haskell, Erlang, Elixir, Rust). Python's PEP 634 brought it to Python 3.10, and it is one of the most significant language additions in Python's recent history.
Part 1 - Syntax and Evaluation Order
The basic syntax:
match subject:
case pattern1:
# runs if subject matches pattern1
...
case pattern2:
# runs if subject matches pattern2 (and not pattern1)
...
case _:
# wildcard - matches anything
...
Key rules:
- Python evaluates cases top to bottom, stopping at the first match
- Each
caseis tried in order - only one executes - If no case matches and there is no
case _:, the match statement does nothing (no error) case _:is not required but is strongly recommended to make intent explicit
# Minimal working example
def http_status(code):
match code:
case 200:
return "OK"
case 201:
return "Created"
case 400:
return "Bad Request"
case 404:
return "Not Found"
case 500:
return "Internal Server Error"
case _:
return f"Unknown status: {code}"
print(http_status(200)) # OK
print(http_status(418)) # Unknown status: 418
This looks like a switch statement - but you have only seen the surface.
Part 2 - Literal Patterns
Literal patterns match exact values. Python compares the subject against the literal using equality (==).
def describe_value(x):
match x:
case 0:
return "zero integer"
case 0.0:
return "zero float"
case True:
return "boolean True"
case False:
return "boolean False"
case None:
return "None"
case "":
return "empty string"
case _:
return f"something else: {x!r}"
# Caution: Python's equality for booleans
print(describe_value(True)) # "boolean True" - True matches case True
print(describe_value(1)) # "boolean True" - because 1 == True in Python!
print(describe_value(0)) # "zero integer" - 0 == False but case 0 comes first
:::warning Boolean Equality Trap
In Python, True == 1 and False == 0. Literal patterns use ==, so case True: will match the integer 1. Order your literal cases carefully when mixing booleans and integers. Place case True: and case False: before case 1: and case 0: if you need to distinguish them.
:::
Supported literals: integers, floats, strings, bytes, None, True, False. You cannot use arbitrary expressions as literals in patterns - case x + 1: is a syntax error.
Part 3 - Capture Patterns and the Single Greatest Pitfall
A capture pattern is a bare name - and this is where most engineers first get burned.
x = 42
match some_value:
case x: # THIS DOES NOT compare to the variable x
print(f"captured: {x}") # x is now rebound to some_value
case x: does not compare the subject against the variable x. It captures (binds) the subject into the name x. A bare name in a pattern is always a capture, never a lookup. It matches anything - equivalent to case _: but gives you the value.
STATUS_OK = 200 # a module-level constant
def handle(code):
match code:
case STATUS_OK: # WRONG - this is a CAPTURE pattern!
print("success") # This branch matches EVERYTHING, not just 200
case _:
print("other")
handle(404) # prints "success" - STATUS_OK is now 404!
:::danger The Constant Comparison Trap
case STATUS_OK: does not compare against STATUS_OK. It captures the subject into the name STATUS_OK, shadowing your constant. This is the single most dangerous pitfall in pattern matching. Python raises no error.
To compare against a constant, use a dotted name (class attribute, enum value, or module attribute):
from enum import Enum
class Status(Enum):
OK = 200
NOT_FOUND = 404
def handle(code):
match code:
case Status.OK: # dotted name - this IS a value comparison
print("success")
case Status.NOT_FOUND:
print("not found")
case _:
print("other")
:::
Part 4 - Wildcard Pattern
case _: is the wildcard. It matches anything but does not bind a name. Think of it as "match everything, care about nothing."
def classify(value):
match value:
case 0:
return "zero"
case _: # matches anything that is not 0
return "non-zero"
The wildcard is also used inside compound patterns to mean "any value in this position":
match point:
case (_, 0): # any x, y must be 0
return "on x-axis"
case (0, _): # x must be 0, any y
return "on y-axis"
case (_, _): # any x, any y
return "off-axis"
Part 5 - OR Patterns
Use | to combine multiple patterns in a single case:
def classify_http_error(code):
match code:
case 400 | 401 | 403:
return "client auth problem"
case 404:
return "not found"
case 500 | 502 | 503:
return "server error"
case _:
return "unclassified"
print(classify_http_error(401)) # "client auth problem"
print(classify_http_error(503)) # "server error"
:::note OR Pattern Binding Restriction
All alternatives in an OR pattern must bind the same names. case [x] | [x, y]: is a syntax error because one alternative binds x and y but the other only binds x. Python enforces this at parse time.
:::
Part 6 - Sequence Patterns
Sequence patterns match lists, tuples, and any object implementing the sequence protocol. They allow destructuring - extracting sub-parts by position.
case [first, second, third]: - Matches ONLY sequences of exactly 3 elements. Captures first, second, third by position.
case [head, *tail]: - Matches any sequence with at least 1 element. Captures head (first) and tail (all remaining elements as a list).
def analyze_command_args(args):
"""Parse a list of command-line argument tokens."""
match args:
case []:
return "no arguments"
case [single]:
return f"one argument: {single}"
case ["--verbose", *rest]:
return f"verbose mode, remaining args: {rest}"
case [first, second]:
return f"exactly two args: {first}, {second}"
case [first, *rest]:
return f"starts with {first!r}, {len(rest)} more args"
print(analyze_command_args([])) # no arguments
print(analyze_command_args(["--verbose", "a", "b"])) # verbose mode, remaining args: ['a', 'b']
print(analyze_command_args(["run", "tests"])) # exactly two args: run, tests
print(analyze_command_args(["build", "a", "b", "c"])) # starts with 'build', 3 more args
:::warning Sequence Pattern vs Tuple Literal
case (x, y): and case [x, y]: both match a two-element sequence. However, the pattern case (x, y): does not require the subject to be a tuple - it matches any two-element sequence including lists. Use [] or () interchangeably for sequence patterns, as they are semantically identical. If you need to match only a tuple, use a class pattern: case tuple() as t if len(t) == 2:.
:::
Part 7 - Mapping Patterns
Mapping patterns match dictionaries (and any object implementing the mapping protocol). Unlike sequence patterns, mapping patterns perform partial matching - extra keys in the subject are silently ignored.
def process_api_event(event):
"""Dispatch on the structure of an incoming API event payload."""
match event:
case {"type": "user_created", "user_id": uid, "email": email}:
return f"Create user {uid} with email {email}"
case {"type": "user_deleted", "user_id": uid}:
return f"Delete user {uid}"
case {"type": "order_placed", "order_id": oid, "amount": amount}:
return f"Order {oid} for ${amount:.2f}"
case {"type": event_type}:
return f"Unhandled event type: {event_type}"
case _:
return "Malformed event - missing 'type' key"
# The mapping pattern only requires the listed keys to be present
event = {"type": "user_created", "user_id": "u123", "email": "[email protected]", "extra": "ignored"}
The fact that extra keys are ignored is intentional and powerful - API payloads often contain fields your handler does not care about. You only declare the keys you need.
# Capture the remaining keys with **rest
match config:
case {"log_level": level, **rest}:
print(f"Level: {level}, other config: {rest}")
:::tip When Mapping Patterns Excel
Mapping patterns are ideal for processing JSON API responses, webhook payloads, configuration dictionaries, and message queues where the payload type is embedded as a key inside the dictionary. They replace verbose chains of .get() calls and isinstance(event, dict) checks.
:::
Part 8 - Class Patterns
Class patterns match instances of a specific class by checking isinstance and then matching named attributes.
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Circle:
center: Point
radius: float
@dataclass
class Rectangle:
top_left: Point
bottom_right: Point
def describe_shape(shape):
match shape:
case Circle(center=Point(x=0, y=0), radius=r):
return f"Circle of radius {r} centered at origin"
case Circle(center=Point(x=cx, y=cy), radius=r):
return f"Circle of radius {r} at ({cx}, {cy})"
case Rectangle(top_left=Point(x=x1, y=y1), bottom_right=Point(x=x2, y=y2)):
width = abs(x2 - x1)
height = abs(y2 - y1)
return f"Rectangle {width}x{height}"
case _:
return "Unknown shape"
c1 = Circle(center=Point(0, 0), radius=5)
c2 = Circle(center=Point(3, 4), radius=2)
r1 = Rectangle(top_left=Point(0, 10), bottom_right=Point(5, 0))
print(describe_shape(c1)) # Circle of radius 5 centered at origin
print(describe_shape(c2)) # Circle of radius 2 at (3, 4)
print(describe_shape(r1)) # Rectangle 5x10
Notice the nesting: Circle(center=Point(x=0, y=0), ...) is a class pattern nested inside another class pattern. This is structural matching of a real object graph.
For built-in types, Python provides positional pattern support via __match_args__:
@dataclass
class Point:
x: float
y: float
# dataclass automatically sets __match_args__ = ('x', 'y')
match p:
case Point(0, 0): # positional - matches Point where x=0, y=0
print("origin")
case Point(x, 0): # positional - captures x, requires y=0
print(f"x-axis at {x}")
Part 9 - Guard Clauses in Patterns
Any pattern can include an if guard - an additional boolean condition evaluated after the structural match succeeds.
def classify_point(point):
match point:
case (x, y) if x == y:
return f"On diagonal at ({x}, {y})"
case (x, y) if x > 0 and y > 0:
return f"First quadrant: ({x}, {y})"
case (x, y) if x < 0 and y < 0:
return f"Third quadrant: ({x}, {y})"
case (x, y):
return f"Other quadrant: ({x}, {y})"
print(classify_point((3, 3))) # On diagonal at (3, 3)
print(classify_point((2, 5))) # First quadrant: (2, 5)
print(classify_point((-1, -4))) # Third quadrant: (-1, -4)
Guards execute only when the pattern matches structurally. If the guard fails, Python continues to the next case.
:::note Guard Scope
Variables captured by the pattern are available inside the guard expression. In case (x, y) if x == y:, both x and y are bound before the guard is evaluated.
:::
Part 10 - Nested Patterns
All pattern types compose freely. This is where structural matching truly outshines if/elif chains.
# A real-world CLI command dispatcher
def dispatch(command):
"""
Expects commands like:
{"cmd": "read", "path": "/etc/hosts"}
{"cmd": "write", "path": "/tmp/out.txt", "content": "hello"}
{"cmd": "copy", "src": "/a", "dst": "/b"}
{"cmd": "search", "paths": ["/etc", "/usr"], "pattern": "*.conf"}
"""
match command:
case {"cmd": "read", "path": str(path)}:
return f"Read file: {path}"
case {"cmd": "write", "path": str(path), "content": str(content)}:
return f"Write to {path}: {content!r}"
case {"cmd": "copy", "src": str(src), "dst": str(dst)}:
return f"Copy {src} → {dst}"
case {"cmd": "search", "paths": [*paths], "pattern": str(pattern)}:
return f"Search {len(paths)} paths for {pattern!r}"
case {"cmd": str(unknown_cmd)}:
return f"Unknown command: {unknown_cmd!r}"
case _:
return "Malformed command - missing 'cmd' key"
print(dispatch({"cmd": "read", "path": "/etc/hosts"}))
# Read file: /etc/hosts
print(dispatch({"cmd": "search", "paths": ["/etc", "/usr"], "pattern": "*.conf"}))
# Search 2 paths for '*.conf'
print(dispatch({"cmd": "explode"}))
# Unknown command: 'explode'
Part 11 - match vs if/elif: When Each Wins
Use match/case when | Use if/elif when |
|---|---|
| Branching depends on data shape | Branching depends on computed values |
| Destructuring sub-parts of data | Complex boolean algebra (and/or/not) |
| Multiple object types, same logic | Simple range checks (x > 0 and x < 10) |
| Parsing command objects, messages | Conditions involve external function calls |
| JSON/dict payloads with type keys | Targeting Python 3.9 or below |
| AST node dispatch | Logic involves side effects per condition |
| Class hierarchy dispatch | One or two simple equality checks |
# match wins: shape-based dispatch on a dict payload
match event:
case {"type": "click", "x": x, "y": y}:
handle_click(x, y)
case {"type": "keypress", "key": key}:
handle_key(key)
case {"type": "resize", "width": w, "height": h}:
handle_resize(w, h)
# if/elif wins: simple range-based grading
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
else:
grade = "F"
Part 12 - Performance Internals
CPython 3.10+ compiles match statements. For cases involving only literal values, the compiler generates a dispatch table similar to a jump table. This makes literal-only match statements significantly faster than equivalent if/elif chains for large numbers of cases.
# CPython can optimise this to a jump table
match status_code:
case 200: return "OK"
case 201: return "Created"
case 204: return "No Content"
case 301: return "Moved Permanently"
case 302: return "Found"
case 304: return "Not Modified"
case 400: return "Bad Request"
case 401: return "Unauthorized"
case 403: return "Forbidden"
case 404: return "Not Found"
case 500: return "Internal Server Error"
case _: return "Unknown"
For mapping and class patterns, the overhead is comparable to explicit isinstance and attribute access. Guard patterns prevent jump-table optimisation for that case.
:::note Python Version Requirement
match/case requires Python 3.10 or higher. from __future__ import ... does not help - there is no backport at the syntax level. If your production environment targets Python 3.9 or below, you must use if/elif for all branching logic. The structural decomposition idioms can still be replicated with isinstance, .get(), and unpacking assignments.
:::
AI/ML Real-World Connection
Structural pattern matching is a natural fit for processing model outputs when they can have different shapes or types.
from dataclasses import dataclass
from typing import Union
@dataclass
class ClassificationResult:
label: str
confidence: float
@dataclass
class DetectionResult:
boxes: list
labels: list
scores: list
@dataclass
class ErrorResult:
code: str
message: str
def handle_model_output(result: Union[ClassificationResult, DetectionResult, ErrorResult]):
match result:
case ClassificationResult(label=label, confidence=conf) if conf >= 0.9:
return f"High-confidence classification: {label} ({conf:.1%})"
case ClassificationResult(label=label, confidence=conf):
return f"Low-confidence classification: {label} ({conf:.1%}) - needs review"
case DetectionResult(boxes=boxes, labels=labels) if len(boxes) == 0:
return "Detection complete - no objects found"
case DetectionResult(boxes=boxes, labels=labels):
return f"Detected {len(boxes)} objects: {labels}"
case ErrorResult(code="OOM", message=msg):
return f"Out of memory - reduce batch size. Details: {msg}"
case ErrorResult(code=code, message=msg):
return f"Model error [{code}]: {msg}"
Interview Questions
Q1: How is Python's match/case different from a switch statement in C or Java?
Answer: A C/Java switch statement compares a single value against a set of literal constants. Python's match/case performs structural pattern matching - it tests whether data has a particular shape and simultaneously extracts sub-components from it. For example, case {"type": "click", "x": x, "y": y}: does not just test equality - it checks that the subject is a mapping with at least those keys, and binds the values of "x" and "y" to local variables x and y in the same step. No switch statement in C or Java can do that. Python's match is closer to Rust's match, Haskell's pattern matching, or Elixir's function clauses than to a traditional switch.
Q2: What is the difference between a capture pattern case x: and the wildcard case _:?
Answer: Both match anything - neither performs a value comparison. The difference is binding: case _: matches the subject but discards it; the name _ is never bound (or is treated as a throwaway by convention). case x: matches the subject and binds it to the name x, making the value available in the case body. case x: is useful when you need a default branch that also needs to reference the unmatched value, for example: case unknown: logging.warning(f"Unhandled value: {unknown}").
Q3: How do you use a guard condition in a pattern, and when does it evaluate?
Answer: A guard is an if clause appended after the pattern: case (x, y) if x > 0:. Evaluation is two-phase: first Python checks whether the structure matches (is the subject a two-element sequence?); if yes, captured variables are bound (x, y), then the guard expression is evaluated. If the guard is False, the case does not match and Python continues to the next case. This means guards can reference variables captured by the pattern, which is their primary use case - adding conditions that depend on extracted values.
Q4: Mapping patterns only require certain keys to be present - what happens to extra keys in the subject dict?
Answer: Extra keys are silently ignored. case {"type": "click", "x": x}: will match a dictionary that contains "type", "x", and any number of additional keys. This is intentional - real-world payloads (JSON APIs, Kafka messages, configuration dictionaries) routinely include metadata, timestamps, or other fields that individual handlers do not need. You can capture the remaining keys using **rest: case {"type": t, **rest}: binds rest to a dict of all keys not explicitly listed.
Q5: How do class patterns work, and what is __match_args__?
Answer: A class pattern like case Point(x=0, y=y): performs three checks: (1) isinstance(subject, Point) must be True; (2) subject.x == 0 must be True (literal match on attribute); (3) subject.y is captured into y. The keyword form (x=0) is always available. The positional form (case Point(0, y):) requires __match_args__ - a class-level tuple that maps positional pattern arguments to attribute names. Dataclasses automatically generate __match_args__ from their field order. For regular classes, you must define it manually: class Point: __match_args__ = ("x", "y").
Q6: When should you prefer if/elif over match/case?
Answer: Prefer if/elif when: (1) you are targeting Python 3.9 or below - match requires 3.10+; (2) your conditions involve complex boolean algebra with and, or, not, and multiple unrelated variables - if/elif is clearer; (3) you are doing simple range checks (if 0 <= x <= 100) where structural decomposition adds no value; (4) the condition logic involves external function calls or side effects that need to be controlled explicitly. Use match/case when you are dispatching on the shape or type of a data structure - especially nested dictionaries, class hierarchies, or command objects - because it extracts structure and dispatches simultaneously in a readable, declarative way.
Quick Reference Cheatsheet
| Pattern type | Syntax example | What it matches | Binds |
|---|---|---|---|
| Literal | case 42: | Subject equals 42 | Nothing |
| Wildcard | case _: | Anything | Nothing |
| Capture | case x: | Anything | Subject → x |
| OR | case 400 | 401 | 403: | Any listed value | Nothing |
| Sequence (exact) | case [a, b]: | Exactly 2-element sequence | a, b |
| Sequence (star) | case [head, *tail]: | 1+ element sequence | head, tail |
| Mapping | case {"k": v}: | Dict with key "k" | v |
| Mapping (rest) | case {"k": v, **rest}: | Dict with key "k" | v, rest |
| Class | case Point(x=x, y=y): | Instance of Point | x, y |
| Guard | case (x, y) if x > 0: | Sequence + condition | x, y |
| Nested | case {"pt": Point(x=x)}: | Composed match | x |
Graded Practice Challenges
Level 1 - Predict the Output
value = [1, 2, 3, 4]
match value:
case [1, 2]:
print("exactly [1, 2]")
case [1, 2, *rest]:
print(f"starts with 1, 2 - rest: {rest}")
case [first, *_]:
print(f"starts with {first}")
case _:
print("no match")
Show Answer
Output:
starts with 1, 2 - rest: [3, 4]
The first case [1, 2] requires exactly two elements - [1, 2, 3, 4] has four, so it fails. The second case [1, 2, *rest] requires at least two elements starting with 1 and 2, and captures the remainder into rest. The subject starts with 1 and 2, and rest captures [3, 4]. This case matches and executes. The remaining cases are never evaluated.
Level 2 - Debug the Code
Find the bug in this command dispatcher:
EXIT_CODE = 0
SUCCESS = 200
def handle(response):
match response:
case EXIT_CODE:
print("exit")
case SUCCESS:
print("success")
case _:
print(f"unhandled: {response}")
handle(200)
handle(404)
Expected: handle(200) prints "exit" - wait, that's wrong. The expected output is "success" for 200. Why does it not work, and how do you fix it?
Show Answer
Bug: case EXIT_CODE: and case SUCCESS: are capture patterns, not constant comparisons. The bare names EXIT_CODE and SUCCESS are not looked up as variables - they are treated as wildcard captures. case EXIT_CODE: matches everything (just like case _:, but binding the value to the name EXIT_CODE). This means handle(200) hits the first case and prints "exit".
Fix: Use dotted names (e.g., via an Enum or a class):
from enum import IntEnum
class Code(IntEnum):
EXIT = 0
SUCCESS = 200
def handle(response):
match response:
case Code.EXIT:
print("exit")
case Code.SUCCESS:
print("success")
case _:
print(f"unhandled: {response}")
handle(200) # success
handle(0) # exit
handle(404) # unhandled: 404
Alternatively, compare using guard patterns: case x if x == EXIT_CODE:.
Level 3 - Design Challenge
Implement a CLI command parser using structural pattern matching. The parser receives a list of tokenised arguments (strings) and must dispatch to the correct handler.
Support these command formats:
["help"]- print usage["exit"]or["quit"]- exit the program["read", filepath]- read a file atfilepath["write", filepath, content]- writecontenttofilepath["search", "--recursive", directory, pattern]- recursive search["search", directory, pattern]- non-recursive search[]- empty input- Anything else - print "Unknown command"
Use only match/case for dispatching. Each handler should print what it would do.
Show Reference Solution
def execute(tokens: list[str]) -> None:
"""
CLI command dispatcher using structural pattern matching.
tokens: list of string arguments from the command line.
"""
match tokens:
case []:
print("(no input - enter 'help' for usage)")
case ["help"]:
print("Commands: help, exit, quit, read <path>, write <path> <content>, search [--recursive] <dir> <pattern>")
case ["exit"] | ["quit"]:
print("Exiting.")
case ["read", filepath]:
print(f"Reading file: {filepath!r}")
case ["write", filepath, content]:
print(f"Writing to {filepath!r}: {content!r}")
case ["search", "--recursive", directory, pattern]:
print(f"Recursive search in {directory!r} for pattern {pattern!r}")
case ["search", directory, pattern]:
print(f"Non-recursive search in {directory!r} for pattern {pattern!r}")
case ["read"]:
print("Error: 'read' requires a filepath argument")
case ["write", _]:
print("Error: 'write' requires both filepath and content")
case ["search"]:
print("Error: 'search' requires at least a directory and pattern")
case [cmd, *_]:
print(f"Unknown command: {cmd!r}")
# Tests
execute([])
execute(["help"])
execute(["exit"])
execute(["quit"])
execute(["read", "/etc/hosts"])
execute(["write", "/tmp/out.txt", "hello world"])
execute(["search", "/usr", "*.conf"])
execute(["search", "--recursive", "/etc", "*.conf"])
execute(["read"])
execute(["deploy", "production"])
Output:
(no input - enter 'help' for usage)
Commands: help, exit, quit, read <path>, write <path> <content>, search [--recursive] <dir> <pattern>
Exiting.
Exiting.
Reading file: '/etc/hosts'
Writing to '/tmp/out.txt': 'hello world'
Non-recursive search in '/usr' for pattern '*.conf'
Recursive search in '/etc' for pattern '*.conf'
Error: 'read' requires a filepath argument
Unknown command: 'deploy'
Why pattern matching wins here: Each case describes the exact shape of the token list - length, fixed strings at specific positions, and captured values all in one line. An equivalent if/elif chain would require manual len(tokens) checks, index accesses with guard bounds checks, and explicit string comparisons - roughly 3x more code with significantly more risk of off-by-one errors.
Key Takeaways
- Structural pattern matching matches the shape of data, not just its value - it is not a switch statement
case x:is a capture pattern that binds the subject tox- it does not compare against a variable namedx- To compare against a constant, use a dotted name from an enum or class:
case Status.OK: - Mapping patterns match partially - extra keys are ignored, which is ideal for real-world JSON payloads
- Sequence patterns destructure positionally;
*restcaptures remaining elements just like starred assignment - Class patterns check
isinstanceand then match named attributes - they can be nested for deep object graphs - Guard clauses (
if condition) run after structural matching succeeds and have access to captured variables match/caserequires Python 3.10 or higher - no__future__backport exists; useif/eliffor 3.9 and below- CPython can compile literal-only match statements to a jump table, making them faster than equivalent
if/elifchains - Use
matchwhen dispatching on data structure shape; useif/eliffor boolean algebra and simple range comparisons
