Python Match Case Multiple Patterns Explained
Learn how python match case multiple patterns works with the | operator, covering literals, structural patterns, guards, and edge cases.
Python's match statement supports combining multiple patterns in a single case using the | operator. This is the primary mechanism for python match case multiple patterns — matching several distinct values or structures 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 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 identical 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 solve the name-binding problem when you need to check a condition on a bound value:
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. The tradeoff is that guards are evaluated at runtime as ordinary Python expressions, so they do not benefit from the structural dispatch that pattern matching provides.
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 is unreachable because 2 already matched the first case. Python does not emit a warning for this in all situations, so keeping cases ordered from most specific to least specific is a practical rule.
This ordering behavior also interacts with guards. A case with a failing guard does not stop the search; evaluation continues to the next case. This differs from a pattern that matches and binds names, which stops the search immediately.
Performance Considerations
The match statement compiles to efficient dispatch code. For literal patterns, CPython can use a form of switch-like dispatch, which is typically faster than a chain of if/elif comparisons. OR patterns with a handful of alternatives are still evaluated as a sequence of pattern matches, but the overhead is small because each alternative is a simple comparison.
If you have dozens of exact-value alternatives, a dictionary lookup is often more appropriate:
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.
For class patterns, the match performs an isinstance check followed by attribute access. If you are matching against many different classes, the cost is proportional to the number of cases tried, since each case is checked in order. There is no hash-based dispatch for class patterns.
Common Mistakes and Edge Cases
A frequent mistake is using or instead of |:
# Matches only 1, not 2: case 1 or 2: pass
1 or 2 evaluates to 1 because 1 is truthy, so the case only matches the value 1. This is a silent bug that is easy to miss. Always 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. The same constraint applies to mapping patterns as to sequence patterns: if you bind names inside the alternatives, every alternative must bind the same names.