Pattern Matching with match
match looks like a switch, but it does much more: it checks the shape of data and pulls values out of it in one step.
You will learn
- Literal patterns, the wildcard
_, and|alternatives. - Capture patterns, and the famous "bare name captures everything" pitfall.
- Sequence patterns like
[x, y]and["add", *rest]. - Mapping patterns for dictionaries and class patterns for objects.
- Guards (
ifin a case) andasbindings.
The basic form
A match statement takes a value (the subject) and compares it against case patterns, top to bottom. The first pattern that fits runs its block, and then the whole match ends. There is no fall-through and no break.
case 404:is a literal pattern. It matches ifstatus == 404.case _:is the wildcard. It always matches, so it acts as the "default".- If no case matches and there is no wildcard, nothing happens. That is not an error.
(def defines a function. Chapter 5 covers functions in detail; here they just give each example a name.)
β Java: this resembles Java 21's pattern-matching switch with record patterns, more than the old C-style switch. π¨ JS: there is no direct equivalent; it is like a switch combined with destructuring.
Several values in one case: |
Capture patterns
A bare name in a pattern does not compare anything. It captures: it matches any value and stores it in that variable. The power comes from mixing literals and captures inside a structure, like this tuple:
(f"Y={y}" is an f-string: it puts the value of y inside the text. Chapter 9 covers them fully.)
Think of (0, y) as a stencil: "a two-item sequence whose first item is 0. Whatever the second item is, call it y." It is like unpacking assignment, (x, y) = point, but with checks built in.
β οΈ The #1 pitfall: a bare name never compares. Writing a constant's name as a pattern doesn't check against the constant. It captures into a variable with that name, so it matches everything.
β Wrong: captures, matches anything
β Right: dotted names compare
Python catches the obvious case above as a SyntaxError, but only when another case follows. Names with a dot (Keys.QUIT, Color.RED) are treated as value patterns and compared with ==. Enums (Chapter 14) are the usual source of dotted constants.
Sequence patterns
List and tuple patterns match any sequence (a list, a tuple, a rangeβ¦) of the right length. A starred name *rest soaks up any number of remaining items, just like in unpacking.
π Note: strings, bytes and bytearrays are not treated as sequences by match. case [a, b]: will not match the string "hi". This stops many surprises.
Guards
A guard is an if after the pattern. The case only wins if the pattern fits and the guard is true. If the guard is false, matching moves on to the next case.
Mapping patterns (dictionaries)
A pattern written with { } matches a dictionary (Chapter 7) that has at least those keys. Extra keys are ignored. **rest collects them if you want them. This is ideal for JSON-like data.
Class patterns
You can match objects by their class (Chapter 11) and check or capture their attributes. Point(x=0, y=y) means "an instance of Point whose x equals 0; capture its y".
Dataclasses also allow positional class patterns, because they set a special attribute called __match_args__. So case Point(0, y): works too. Built-in types work the same way: case int(n):, case str(s):.
β οΈ Pitfall: case int: (no parentheses) is a capture named int. It matches everything and hides the built-in name. Always write case int(): to check the type.
Naming a sub-pattern with as
pattern as name matches the pattern and also stores what it matched under name. It is useful with alternatives.
Pattern cheat sheet
| Pattern | Matches | Binds |
|---|---|---|
404, "add", None, True | equal value (None/True/False by identity) | β |
Color.RED | equal to that dotted constant | β |
name | anything | name |
_ | anything | nothing |
[a, b] / (a, b) | a sequence of exactly 2 items (not a str) | a, b |
[first, *rest] | a sequence of 1 or more items | first, rest (a list) |
{"k": v} | a mapping with at least key "k" | v |
Point(x=0) | an instance of Point with .x == 0 | β |
p1 | p2 | either pattern | the same names in both |
p as n | pattern p | n = the matched value |
p if cond | pattern p and cond true | as in p |
π‘ When to use which: for simple value checks (x > 10, ranges, booleans) use if/elif. Use match when you check structure and unpack at the same time: commands, parsed JSON, tokens, or tree nodes.
More examples
1. A tiny calculator
2. Validating a config dictionary
3. Summing a nested list recursively
4. Expense records as tuples
5. Matching shapes from a list of dicts
(The last line uses a list comprehension, a one-line loop that builds a list, covered in Chapter 6.)
Recap
match subject:tries eachcasein order. The first match wins, with no fall-through.case _:is the default.- Literals compare. Bare names capture (they never compare!). Dotted names compare.
[a, *rest]matches sequences (not strings), and{"k": v}matches dicts that have at least those keys.Cls(attr=pattern)checks the type and its attributes. Writeint(), notint.|gives alternatives,asnames a sub-match, andifadds a guard.
Exercises
Exercise 1 π’ easy build it
Write day_type(day) using match: "sat" or "sun" gives "weekend", any other string gives "weekday".
Show solution
| joins two literal patterns, and _ covers everything else.
Exercise 2 π’ easy predict the output
What does f([1, 2, 3]) return?
Show solution
first=1, rest=[2, 3]. The first two patterns need exactly 1 or exactly 2 items. *rest always captures a list.
Exercise 3 π’ easy explain why
Why does case [a, b]: not match the string "hi", even though a string is a sequence of characters?
Show solution
Sequence patterns deliberately exclude str, bytes and bytearray. Otherwise a command like "go" would accidentally match a two-item pattern. Use case str(s): or split the string first.
Exercise 4 π’ easy fix the bug
This always prints "stop!". Why? Fix it.
Show solution
case STOP: is a capture pattern: it assigns key to a new variable STOP and always matches. Use a literal, or a dotted name such as Keys.STOP.
Exercise 5 π‘ medium predict the output
What are the outputs for g({"a": 1}), g({"a": 1, "b": 2}) and g({"b": 2})?
Show solution
a=1 plus {} | a=1 plus {'b': 2} | b=2. Mapping patterns allow extra keys, and **rest collects them. The second dict matches the first case, so the second case is never tried.
Exercise 6 π‘ medium refactor it
Rewrite this if chain with match and sequence patterns.
Show solution
Each pattern checks the length and the literal in one go, and names the parts at the same time.
Exercise 7 π‘ medium build it
Write classify(n) that returns "zero", "negative", "small" (1β9) or "big" using one match with guards.
Show solution
A capture plus a guard tests a condition on the value. For pure numeric ranges like this, if/elif is just as clear.
Exercise 8 π‘ medium build it Β· playground
Create playground/commands.py with a function handle(line) for the expense tracker. Support add AMOUNT CATEGORY, add AMOUNT (category defaults to "misc"), total, and quit. It returns a description string. Test it with a list of sample lines.
Show solution
Cases are tried in order, so the three-word add comes before the two-word one. Their lengths differ anyway, so here the order isn't critical.
Exercise 9 π΄ hard predict the output
What does this print?
Show solution
(a, b) and [a, b] are the same pattern: both match any 2-item sequence, list or tuple. The string is not a sequence for matching purposes, so it falls through to _.
Exercise 10 π΄ hard mini-project
Write evaluate(tree) for a tiny expression language stored as nested tuples: ("num", 5), ("add", left, right), ("mul", left, right) and ("neg", x). evaluate(("add", ("num", 2), ("mul", ("num", 3), ("neg", ("num", 4))))) should return -10. Raise ValueError for anything unknown.
Show solution
Each case recognises one node shape and unpacks its children in the same step. This is exactly the job match was designed for: walking tree-shaped data.