Yesterday's match handled string status codes and dict shapes. I want to push on a gap. What happens when the command Diane sends isn't a string — it's a tuple? Something like ("restock", "SKU-1001", 50)?
With yesterday's match I'd check equality against the whole tuple — but that only matches that one exact tuple. I'd need to extract the SKU and quantity separately. Back to index arithmetic.
And that's where you'd reach for if isinstance checks and command[1], command[2] — which works, and is also miserable to read when Diane adds a fourth warehouse parameter and you're staring at command[3] trying to remember what position three is.
I have been that person. I am currently that person in the codebase from three weeks ago.
Today fixes it. Python's match supports sequence patterns. You write the shape of the tuple you expect, with variable names where you want Python to capture values, and the runtime checks the shape and extracts the pieces in one step.
So instead of if command[0] == "restock" and len(command) == 3, I can just describe the tuple I want?
Exactly. Here's the syntax:
match command:
case ("restock", sku, qty):
print(f"SKU is {sku}, quantity is {qty}")Python checks: is command a sequence of length three? Is the first element "restock"? If both are true, it binds sku to command[1] and qty to command[2]. No index arithmetic. No length check. The pattern is the documentation.
The literal "restock" acts as a constraint, and the bare names sku and qty act as capture slots. Python figures out which is which?
This is the most important rule: in a sequence pattern, a bare name is always a capture variable — Python binds it to whatever is at that position. A literal — string, int, None, True, False — is a constraint that must match exactly. Never use a bare name when you mean a constant.
So case ("restock", "SKU-1001", qty) would only match that one exact SKU, while qty captures whatever quantity was passed. Mix and match.
Correct. Now here's the full dispatch table Diane needs. Her protocol sends tuples with a verb in position zero and the rest depends on the verb:
def dispatch_command(command: tuple) -> str:
match command:
case ("restock", sku, qty):
return f"Restock {qty} units of {sku}"
case ("transfer", sku, qty, warehouse):
return f"Transfer {qty} units of {sku} to {warehouse}"
case ("audit", warehouse):
return f"Audit all inventory in {warehouse}"
case ("audit",):
return "Audit all warehouses"
case ():
return "No command provided"
case _:
return "Unknown command"The restock case: three elements — verb, SKU, quantity. The transfer case: four elements. Two audit cases — one with a warehouse, one with just the verb. Then empty tuple, then wildcard.
And the order matters. If the two audit cases were flipped, the two-element version would be unreachable — ("audit", warehouse) would always match first. More specific cases go above more general ones.
Python won't warn me that a case is unreachable?
No warning. It'll happily skip the unreachable case silently. The rule is: specific before general, wildcard at the bottom. This is the discipline that makes sequence patterns safe.
("audit",) — the trailing comma is intentional? It's not a typo?
Completely intentional and frequently confusing. In Python, ("audit") is just the string "audit" in parentheses — not a tuple. The trailing comma is what makes it a one-element tuple. This is standard Python, not match-specific — but match makes it more visible because you're writing tuple patterns constantly.
So if I forget the comma in case ("audit",): and write case ("audit"): instead — what happens?
A SyntaxError in this context — the match parser sees ("audit") as a grouping, not a sequence pattern. The comma is mandatory for one-element patterns. Two or more elements and you don't need it: ("restock", sku, qty) is unambiguously a sequence.
Three rules: literals constrain, names capture. Specific cases above general ones. One-element tuple patterns need the trailing comma.
Those are the three rules that trip up every developer the first time. You've got them before making the mistakes. Now — there's a discard pattern I want to show you. _ in a position means "must exist, value ignored":
match command:
case ("restock", sku, qty) if qty > 0:
return f"Restock {qty} units of {sku}"
case ("restock", _, _):
return "Restock quantity must be positive"_ matches anything at that position without binding it. The second case uses it to say "I see the SKU and quantity but I don't need them — I only care that the shape matches."
_ vs bare name: both accept anything at that position, but _ is the "I'm deliberately ignoring this" signal — readable to the next developer and to linters.
What about variable-length commands? Like ("batch-restock", "SKU-1", 10, "SKU-2", 20, "SKU-3", 30) — an arbitrary number of pairs after the verb?
The star syntax: case ("batch-restock", *items):. Same * from function parameters and unpacking, applied to a pattern. items binds to a list of everything after the verb. Fixed-length patterns can't handle open-ended sequences — *name handles the tail.
Same * everywhere. Function definitions, tuple unpacking, call-site spreading, now pattern matching. One operator, one direction — "collect the rest."
You traced the through-line without me pointing it out. That's the pattern recognition that separates fluent Python from mechanical Python. Now write dispatch_command. Six cases, no guards needed for today — just the shape-based dispatch.
restock is three elements, transfer is four, audit has two cases — with warehouse and without — then empty tuple, then wildcard. I've got the structure.
Sequence patterns match the shape of a tuple or list by position. Literals constrain, bare names capture, _ discards. Order determines priority — Python tests cases top to bottom and takes the first match.
def dispatch_command(command: tuple) -> str:
match command:
case ("restock", sku, qty):
return f"Restock {qty} units of {sku}"
case ("transfer", sku, qty, warehouse):
return f"Transfer {qty} units of {sku} to {warehouse}"
case ("audit", warehouse):
return f"Audit all inventory in {warehouse}"
case ("audit",): # trailing comma — one-element tuple
return "Audit all warehouses"
case ():
return "No command provided"
case _:
return "Unknown command"Three rules for sequence patterns:
"restock" in a pattern must equal "restock" in the subject — exact match.sku, qty, warehouse bind to whatever is at their position — they accept any value._ discards. Matches anything without binding — signals "I see this value but I don't need it."One-element tuples: ("audit",) — the trailing comma is mandatory. ("audit") is a parenthesized string, not a tuple. The comma is what makes it a sequence pattern.
Variable-length sequences: case ("batch", *items) captures all elements after "batch" into a list called items. *name can appear once per pattern and captures all remaining (or preceding) elements.
Guards: case ("restock", sku, qty) if qty > 0 — the if clause runs after the pattern matches. If the guard is False, Python tries the next case. Guards validate values; patterns validate shape.
Pitfall 1: More general case above more specific. If case ("audit", warehouse) appears before case ("audit",), the one-element audit is unreachable — the two-element pattern would fail to match it (wrong length) but Python would fall to the wildcard. Always put specific cases first.
Pitfall 2: Forgetting the trailing comma. case ("audit"): is a SyntaxError — Python sees a parenthesized expression, not a sequence pattern. Write case ("audit",):.
Pitfall 3: Using a variable name as a constant. If STATUS = "active" and you write case STATUS:, Python does NOT check if the subject equals STATUS — it captures the subject into a new variable called STATUS, overwriting the outer binding. Use qualified names (module.CONSTANT) or guard clauses for constant checks.
Class patterns match object types: case Point(x=x, y=y) matches instances of Point and binds attribute values. The class must either be a dataclass/namedtuple or define __match_args__ for positional patterns.
OR patterns combine alternatives: case "express" | "overnight" matches either string. Works in sequence patterns too: case ("express" | "priority", sku, qty).
Nested patterns compose freely: case {"items": [first, *_]} matches a dict with an items key holding a non-empty list, binding only the first element. This is where structural pattern matching becomes genuinely powerful for parsing nested data structures.
Sign up to write and run code in this lesson.
Yesterday's match handled string status codes and dict shapes. I want to push on a gap. What happens when the command Diane sends isn't a string — it's a tuple? Something like ("restock", "SKU-1001", 50)?
With yesterday's match I'd check equality against the whole tuple — but that only matches that one exact tuple. I'd need to extract the SKU and quantity separately. Back to index arithmetic.
And that's where you'd reach for if isinstance checks and command[1], command[2] — which works, and is also miserable to read when Diane adds a fourth warehouse parameter and you're staring at command[3] trying to remember what position three is.
I have been that person. I am currently that person in the codebase from three weeks ago.
Today fixes it. Python's match supports sequence patterns. You write the shape of the tuple you expect, with variable names where you want Python to capture values, and the runtime checks the shape and extracts the pieces in one step.
So instead of if command[0] == "restock" and len(command) == 3, I can just describe the tuple I want?
Exactly. Here's the syntax:
match command:
case ("restock", sku, qty):
print(f"SKU is {sku}, quantity is {qty}")Python checks: is command a sequence of length three? Is the first element "restock"? If both are true, it binds sku to command[1] and qty to command[2]. No index arithmetic. No length check. The pattern is the documentation.
The literal "restock" acts as a constraint, and the bare names sku and qty act as capture slots. Python figures out which is which?
This is the most important rule: in a sequence pattern, a bare name is always a capture variable — Python binds it to whatever is at that position. A literal — string, int, None, True, False — is a constraint that must match exactly. Never use a bare name when you mean a constant.
So case ("restock", "SKU-1001", qty) would only match that one exact SKU, while qty captures whatever quantity was passed. Mix and match.
Correct. Now here's the full dispatch table Diane needs. Her protocol sends tuples with a verb in position zero and the rest depends on the verb:
def dispatch_command(command: tuple) -> str:
match command:
case ("restock", sku, qty):
return f"Restock {qty} units of {sku}"
case ("transfer", sku, qty, warehouse):
return f"Transfer {qty} units of {sku} to {warehouse}"
case ("audit", warehouse):
return f"Audit all inventory in {warehouse}"
case ("audit",):
return "Audit all warehouses"
case ():
return "No command provided"
case _:
return "Unknown command"The restock case: three elements — verb, SKU, quantity. The transfer case: four elements. Two audit cases — one with a warehouse, one with just the verb. Then empty tuple, then wildcard.
And the order matters. If the two audit cases were flipped, the two-element version would be unreachable — ("audit", warehouse) would always match first. More specific cases go above more general ones.
Python won't warn me that a case is unreachable?
No warning. It'll happily skip the unreachable case silently. The rule is: specific before general, wildcard at the bottom. This is the discipline that makes sequence patterns safe.
("audit",) — the trailing comma is intentional? It's not a typo?
Completely intentional and frequently confusing. In Python, ("audit") is just the string "audit" in parentheses — not a tuple. The trailing comma is what makes it a one-element tuple. This is standard Python, not match-specific — but match makes it more visible because you're writing tuple patterns constantly.
So if I forget the comma in case ("audit",): and write case ("audit"): instead — what happens?
A SyntaxError in this context — the match parser sees ("audit") as a grouping, not a sequence pattern. The comma is mandatory for one-element patterns. Two or more elements and you don't need it: ("restock", sku, qty) is unambiguously a sequence.
Three rules: literals constrain, names capture. Specific cases above general ones. One-element tuple patterns need the trailing comma.
Those are the three rules that trip up every developer the first time. You've got them before making the mistakes. Now — there's a discard pattern I want to show you. _ in a position means "must exist, value ignored":
match command:
case ("restock", sku, qty) if qty > 0:
return f"Restock {qty} units of {sku}"
case ("restock", _, _):
return "Restock quantity must be positive"_ matches anything at that position without binding it. The second case uses it to say "I see the SKU and quantity but I don't need them — I only care that the shape matches."
_ vs bare name: both accept anything at that position, but _ is the "I'm deliberately ignoring this" signal — readable to the next developer and to linters.
What about variable-length commands? Like ("batch-restock", "SKU-1", 10, "SKU-2", 20, "SKU-3", 30) — an arbitrary number of pairs after the verb?
The star syntax: case ("batch-restock", *items):. Same * from function parameters and unpacking, applied to a pattern. items binds to a list of everything after the verb. Fixed-length patterns can't handle open-ended sequences — *name handles the tail.
Same * everywhere. Function definitions, tuple unpacking, call-site spreading, now pattern matching. One operator, one direction — "collect the rest."
You traced the through-line without me pointing it out. That's the pattern recognition that separates fluent Python from mechanical Python. Now write dispatch_command. Six cases, no guards needed for today — just the shape-based dispatch.
restock is three elements, transfer is four, audit has two cases — with warehouse and without — then empty tuple, then wildcard. I've got the structure.
Sequence patterns match the shape of a tuple or list by position. Literals constrain, bare names capture, _ discards. Order determines priority — Python tests cases top to bottom and takes the first match.
def dispatch_command(command: tuple) -> str:
match command:
case ("restock", sku, qty):
return f"Restock {qty} units of {sku}"
case ("transfer", sku, qty, warehouse):
return f"Transfer {qty} units of {sku} to {warehouse}"
case ("audit", warehouse):
return f"Audit all inventory in {warehouse}"
case ("audit",): # trailing comma — one-element tuple
return "Audit all warehouses"
case ():
return "No command provided"
case _:
return "Unknown command"Three rules for sequence patterns:
"restock" in a pattern must equal "restock" in the subject — exact match.sku, qty, warehouse bind to whatever is at their position — they accept any value._ discards. Matches anything without binding — signals "I see this value but I don't need it."One-element tuples: ("audit",) — the trailing comma is mandatory. ("audit") is a parenthesized string, not a tuple. The comma is what makes it a sequence pattern.
Variable-length sequences: case ("batch", *items) captures all elements after "batch" into a list called items. *name can appear once per pattern and captures all remaining (or preceding) elements.
Guards: case ("restock", sku, qty) if qty > 0 — the if clause runs after the pattern matches. If the guard is False, Python tries the next case. Guards validate values; patterns validate shape.
Pitfall 1: More general case above more specific. If case ("audit", warehouse) appears before case ("audit",), the one-element audit is unreachable — the two-element pattern would fail to match it (wrong length) but Python would fall to the wildcard. Always put specific cases first.
Pitfall 2: Forgetting the trailing comma. case ("audit"): is a SyntaxError — Python sees a parenthesized expression, not a sequence pattern. Write case ("audit",):.
Pitfall 3: Using a variable name as a constant. If STATUS = "active" and you write case STATUS:, Python does NOT check if the subject equals STATUS — it captures the subject into a new variable called STATUS, overwriting the outer binding. Use qualified names (module.CONSTANT) or guard clauses for constant checks.
Class patterns match object types: case Point(x=x, y=y) matches instances of Point and binds attribute values. The class must either be a dataclass/namedtuple or define __match_args__ for positional patterns.
OR patterns combine alternatives: case "express" | "overnight" matches either string. Works in sequence patterns too: case ("express" | "priority", sku, qty).
Nested patterns compose freely: case {"items": [first, *_]} matches a dict with an items key holding a non-empty list, binding only the first element. This is where structural pattern matching becomes genuinely powerful for parsing nested data structures.