On this page
Description
Python 3.10 added match, the biggest syntax change in years, plus a few smaller tools: X | Y union types, zip(strict=True), parenthesized context managers and much better error messages. match looks like a switch statement from Java or C, and that resemblance causes most of the bugs - it's really structural pattern matching, borrowed from functional languages: it reads the shape of data and pulls pieces out of it.
Here's the kind of bug we mean. A team ports a Java switch on an enum:
RED, GREEN = Color.RED, Color.GREEN
match light:
case RED:
stop()
case GREEN:
go()
It doesn't compile: SyntaxError: name capture 'RED' makes remaining patterns unreachable. In a pattern, a bare name isn't a value to compare against - it's a variable to bind, so case RED: matches anything and assigns it to RED. (Had it been the only case, it would have silently matched everything.)
We'll go from simple to more technical:
matchandcase: first match wins, and no match is silent.- Capture patterns versus value patterns - the bug above.
- Sequence, mapping and class patterns, and guards.
- The rest of 3.10:
X | Y,zip(strict=True), better errors.
match and case
match command:
case ["go", direction]: # a 2-item list starting with "go"
move(direction)
case ["drop", *items]: # "drop" followed by any number of items
drop_all(items)
case {"type": "click", "x": x, "y": y}: # a dict with at least these keys
click(x, y)
case Point(x=0, y=y): # a Point whose x is 0
on_y_axis(y)
case "quit" | "exit": # literals, with alternatives
quit()
case _: # anything else
unknown(command)
The rules:
- First match wins; there's no fall-through and no
break. If no case matches, nothing happens - no exception, no exhaustiveness check (a test'smatchon an enum given the string"red"matched nothing and fell through to the code after it). Addcase _:to handle the rest explicitly, or raise in it. - Capture vs value patterns. A bare name (
direction,x) captures; a dotted name (Color.RED,http.HTTPStatus.OK) or a literal ("quit",404,None) compares with==(isforNone,True,False). That's why constants in patterns must be qualified._matches without binding. - Sequence patterns match lists, tuples and other sequences - but never
str,bytesorbytearray:case [a, b]doesn't match"ab"(a test fell through tocase str():), while("a", "b")and["a", "b"]both match. - Mapping patterns match any mapping with at least the listed keys - extra keys are ignored (a click event with an
"extra"key still matched). Use**restto capture the others. - Class patterns check
isinstance, then attributes:Point(x=0, y=y)matches aPointwhosex == 0and bindsy. Positional sub-patterns (Point(0, 0)) use the class's__match_args__- dataclasses and named tuples generate it (('x', 'y')). Builtins likestr()andint()match by type:case str():. - Guards run after a pattern matches and its names are bound:
case Point(x, y) if x == y:.
Use match when the shape of data drives the logic - parsing commands, JSON events, ASTs, protocol messages. For a plain value lookup, a dict is often clearer.
The rest of 3.10
X | Yunions (PEP 604):def find(id: int) -> User | None, and also at run time -isinstance(5, int | str)isTrue.Optional[int]is justint | None.zip(..., strict=True): plainzipstops at the shortest input and silently drops the rest ([(1, 'a'), (2, 'b')]from three numbers and two letters); withstrict=Trueit raisesValueError: zip() argument 2 is shorter than argument 1. Use it whenever the inputs must line up.- Parenthesized context managers:
with (open(a) as f, open(b) as g):across several lines. - Better error messages: unclosed brackets reported where they open,
Did you mean ...?suggestions for misspelled names and attributes, and precise syntax-error ranges - the start of the "nicer errors" work that 3.11 continued.
Which tool for which job?
| You want to | Use |
|---|---|
| Branch on the shape of data (lists, dicts, objects) | match with sequence, mapping and class patterns |
| Compare against a constant | a dotted name (Color.RED) or a literal in case |
| Map a value to a result | a dict lookup |
| Handle "anything else" | case _: (raise in it if nothing else is valid) |
| An extra condition on a pattern | a guard: case Point(x, y) if x == y: |
| Say "an int or nothing" in a type hint | int | None |
| Pair up lists that must be the same length | zip(a, b, strict=True) |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
case RED: with a module constant | captures everything (or a SyntaxError) | case Color.RED: |
No case _: | unmatched values are silently ignored | add case _: and raise or handle |
case [a, b]: expecting to match "ab" | strings never match sequence patterns | case str(): |
| Expecting a mapping pattern to require exactly the keys | extra keys still match | capture **rest and check it |
case Plain(1): on a class without __match_args__ | TypeError at run time | keyword patterns, or a dataclass |
Plain zip on inputs that must line up | the extras are silently dropped | zip(..., strict=True) |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. Why is case RED: (with RED a module-level constant) a bug?
Show answer
A bare name in a pattern captures: it matches anything and binds it to RED. Use a dotted name (Color.RED) or a literal to compare.
2. Does case {"type": "click"}: match {"type": "click", "x": 4}?
Show answer
Yes - mapping patterns require at least the listed keys and ignore the rest.
3. What does a match do when no case matches?
Show answer
Nothing - execution continues after it. There's no error and no exhaustiveness check at run time; add case _: to handle or reject the rest.
Key takeaways
matchdestructures shapes; first match wins; no match is silent.- Bare names capture - qualify constants.
- Sequences exclude strings; mappings ignore extra keys; class patterns use
__match_args__. - 3.10 also brought
X | Y,zip(strict=True)and better errors. - A dict lookup is still the clearest way to map values to results.