Structural Pattern Matching: Python's match Statement Explained
match/case, added in Python 3.10, compares a value against a series of patterns and runs the first one that fits.- It goes well beyond a simple switch statement: patterns can unpack sequences, match object attributes, and bind variables as they match.
- A bare name in a
casepattern always matches and binds — usecase _:for a true catch-all that binds nothing. ifguard clauses on acaseadd extra conditions beyond what the pattern shape alone can express.
Basic syntax: match and case
A match statement takes a subject value and a series of case clauses, each specifying a pattern to compare against:
def describe_status(code):
match code:
case 200:
return "OK"
case 301 | 302:
return "Redirect"
case 404:
return "Not found"
case 500:
return "Server error"
case _:
return "Unknown status"
print(describe_status(404)) # Not found
print(describe_status(301)) # Redirect
The _ pattern is a wildcard that matches anything and is conventionally placed last, functioning like an else branch. Python checks cases top to bottom and stops at the first match, the same order-dependence as a chain of if/elif statements.
More than a switch statement
Languages with a switch statement typically only compare a value for equality against a list of constants. Python's match does that, but also supports structural patterns that inspect the shape of a value, not just its equality:
def handle(command):
match command.split():
case ["go", direction]:
return f"Moving {direction}"
case ["take", item]:
return f"Picking up {item}"
case ["look"]:
return "Looking around"
case _:
return "Unknown command"
print(handle("go north")) # Moving north
print(handle("take key")) # Picking up key
Each case here matches a specific sequence shape, and the second element of the list is bound to a variable name (direction, item) usable inside that branch. This is closer to pattern matching in languages like Rust or Haskell than to a traditional switch statement.
Matching sequences and unpacking
Sequence patterns can fix some positions and capture the rest with a starred name, similar to unpacking assignment:
def summarize(items):
match items:
case []:
return "empty"
case [single]:
return f"one item: {single}"
case [first, second]:
return f"two items: {first} and {second}"
case [first, *rest]:
return f"starts with {first}, {len(rest)} more after it"
print(summarize([])) # empty
print(summarize([1])) # one item: 1
print(summarize([1, 2, 3, 4])) # starts with 1, 3 more after it
Patterns are also checked for length, so [first, second] only matches sequences with exactly two elements — a three-element list falls through to the next case, exactly as the length mismatch would suggest.
Matching class instances
match can inspect an object's class and pull out attributes in the same pattern, which is particularly useful for handling a family of related shapes, such as different kinds of events:
class Click:
def __init__(self, x, y):
self.x, self.y = x, y
class KeyPress:
def __init__(self, key):
self.key = key
def handle_event(event):
match event:
case Click(x=0, y=0):
return "Clicked the origin"
case Click(x=x, y=y):
return f"Clicked at ({x}, {y})"
case KeyPress(key="Escape"):
return "Cancelled"
case KeyPress(key=key):
return f"Pressed {key}"
print(handle_event(Click(3, 4))) # Clicked at (3, 4)
print(handle_event(KeyPress("Escape"))) # Cancelled
Each case both checks that event is an instance of the given class and destructures its attributes in one step, replacing what would otherwise be a chain of isinstance() checks followed by manual attribute access.
Guard conditions with if
An if clause after a pattern adds a condition that must also hold for the case to match, for logic that a pattern shape alone cannot express:
def classify(n):
match n:
case int() if n < 0:
return "negative integer"
case int() if n == 0:
return "zero"
case int():
return "positive integer"
case _:
return "not an integer"
print(classify(-5)) # negative integer
print(classify(0)) # zero
If the pattern matches but the guard evaluates to False, Python moves on to the next case as if this one had not matched at all, rather than stopping the search.
Combining patterns with |
The | operator inside a pattern combines several alternatives into a single case, matching if any one of them applies:
def day_type(day):
match day:
case "Saturday" | "Sunday":
return "weekend"
case "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday":
return "weekday"
case _:
return "not a day"
print(day_type("Sunday")) # weekend
This reads more directly than an equivalent chain of separate cases, and mirrors how | is used elsewhere in Python's type hints to mean "one of these options," which keeps the meaning consistent across different parts of the language.
Patterns can also be nested and combined freely — a sequence pattern can contain a class pattern, which can itself contain an or-pattern, all in a single case line. This composability is exactly what separates structural pattern matching from a traditional switch statement: a switch only ever asks "does this value equal one of these constants," while match can ask "does this value have this shape, made of pieces that themselves have these shapes," in one readable expression.
As with any feature that adds expressive power, it is worth resisting the urge to force every conditional chain into a match statement. A plain if/elif chain remains perfectly appropriate, and often clearer, when the conditions being tested are unrelated boolean expressions rather than variations on the shape of one value. match earns its keep specifically when you are dispatching on what a value is or how it is structured.