Python Match Case Multiple Patterns Explained
Learn how to combine multiple patterns in a single Python match case with the | operator, covering literals, structural patterns, guards, and edge cases.
Python 3.10 introduced the match statement for structural pattern matching. A single case can combine multiple patterns with the | operator, which is the clean way to match several distinct values or shapes without writing separate cases or falling back to if/elif chains.
Combining Literal Values with the OR Operator
The simplest form of multi-pattern matching combines literal values:
def describe_status(code): match code: case 200 | 201 | 204: return "success" case 400 | 404: return "client error" case 500 | 502 | 503: return "server error" case _: return "unknown"
The | operator acts as a logical OR between patterns. The case matches if any of the alternatives matches. This is equivalent to writing three separate cases, but it keeps the handling in one block and avoids duplicating the body.
You can also mix different types of literals in one OR pattern:
match value: case "yes" | "y" | True: print("affirmative") case "no" | "n" | False: print("negative")
This is useful when normalizing user input or handling aliases for the same logical value.
Matching Multiple Structural Patterns
OR patterns work with more than literals. You can combine sequence patterns, mapping patterns, and class patterns:
match point: case (0, 0): print("origin") case (0, y) | (y, 0): print(f"on an axis at {y}") case _: print("off-axis")
Both alternatives in the second case bind the same name y. This is a hard requirement: every alternative in an OR pattern must bind the same set of names. If one alternative binds x and another binds y, Python raises a SyntaxError at compile time.
Class patterns combine the same way:
match command: case Move(dx=0, dy=0) | Stop(): print("no movement") case Move(dx, dy): print(f"moving by ({dx}, {dy})")
Here Move(dx=0, dy=0) matches a Move instance with both attributes zero, and Stop() matches a Stop instance. Neither alternative binds a name, so the OR pattern is valid.
Name Binding Constraints in OR Patterns
The name-binding constraint deserves attention because it is the most common source of compile errors. Consider:
# This raises SyntaxError: match point: case (0, y) | (x, 0): print("axis")
Both alternatives must bind the same names. The fix is to use the same name in both positions:
match point: case (0, y) | (y, 0): print(f"on an axis at {y}")
The constraint exists because after a match, the bound names must be available in the case body. If different alternatives bound different names, the code following the case would not know which names exist. Python resolves this by requiring the same names across all alternatives.
You can bind the same name at different structural positions, as long as the name appears in every alternative. The value bound is the one from the alternative that actually matched.
Using Guards When OR Patterns Don't Fit
OR patterns express a union of shapes, but some conditions cannot be expressed as a pattern union. Numeric ranges are the classic example:
match value: case x if 1 <= x <= 10: print("small") case x if 11 <= x <= 100: print("medium") case _: print("large")
A guard is the if clause after a pattern. It is evaluated only after the pattern matches, and the case body runs only if both the pattern and the guard succeed. Guards can use any expression: comparisons, in checks, function calls, or attribute access.
Guards also let you state conditions that pattern syntax cannot express directly. For example, to test whether two captured values are equal:
match point: case (x, y) if x == y: print("diagonal") case (x, y): print(f"({x}, {y})")
When you need range checks, cross-field conditions, or conditions that depend on external state, guards are more expressive than OR patterns. A guard is an ordinary Python expression, evaluated at runtime after the pattern itself has matched; it cannot change which structural tests happen during matching.
Runtime Behavior and Evaluation Order
The match statement evaluates cases in order, top to bottom. The first case whose pattern matches — and whose guard, if present, evaluates to true — wins. This means case order matters:
match value: case 1 | 2: print("one or two") case 2: print("two") # unreachable for value == 2
The second case can never run for value == 2, because the first case already matches. Keeping cases ordered from most specific to least specific is a practical rule.
Guards interact with this ordering as well. A case whose pattern matches but whose guard fails does not stop the search; matching continues with the next case. A case whose pattern matches and has a true guard (or no guard) ends the search.
When a Dictionary Is a Better Fit
For a flat mapping from exact values to outcomes, a dictionary is often clearer than a long match statement:
status_messages = { 200: "success", 201: "created", 204: "no content", 400: "bad request", 404: "not found", }
Use match when the structure of the value matters — sequences, mappings, class attributes, or nested combinations. Use a dictionary when you are mapping exact values to outcomes and there is no structural component.
Class patterns use an isinstance check, followed by attribute access for subpatterns. Because cases are tried in order, a long row of class cases behaves like a series of ordered checks rather than a hash lookup.
Common Mistakes and Edge Cases
A frequent mistake is using or instead of |:
match value: case 1 or 2: pass
Patterns are not ordinary expressions, so or is not a valid pattern operator. Use | inside patterns.
Another edge case: combining a wildcard with a specific pattern. case _ | 5: is valid but redundant — the wildcard matches everything. If you want to match "anything except a specific value," a guard is clearer:
match value: case x if x != 5: print(f"not five: {x}") case _: print("five")
Mapping patterns follow the same OR rules. Combining two mapping patterns with | is useful when either key layout is acceptable:
match request: case {"method": "GET"} | {"method": "HEAD"}: print("read-only request") case {"method": "POST"} | {"method": "PUT"}: print("write request")
This binds no names, so the name-binding rule is satisfied automatically. If the alternatives did bind names, every alternative would need to bind the same names, just as with sequence patterns.