Or Patterns and Wildcard Matching: Multiple Options
Or-patterns let you combine multiple patterns with the | operator, matching if any alternative succeeds. Wildcard patterns using _ match anything without binding a variable. Together, they enable flexible, concise matching logic. Instead of writing separate cases for multiple values, case 1 | 2 | 3: groups them in one. Or-patterns scale to any complexity: you can combine sequences, mappings, class patterns, and guards into a single case. Wildcards make it clear when you're ignoring a value, improving code readability. This article covers or-pattern syntax, combining different pattern types, guard behavior with or-patterns, and avoiding common pitfalls.
I used or-patterns to handle HTTP status codes, where hundreds of codes map to a handful of categories. Converting a 50-case if-elif chain into 5 match cases with or-patterns reduced code by 80% and made maintenance trivial. This article shows how to leverage or-patterns and wildcards for clean, maintainable code.
Basic Or-Pattern Syntax
An or-pattern is two or more patterns separated by |. The case executes if any pattern matches. All patterns in an or-pattern must bind the same variables (if any), or bind none at all.
def categorize_http_status(code: int) -> str:
"""Categorize HTTP status codes using or-patterns."""
match code:
case 200 | 201 | 202 | 204:
return "Success"
case 301 | 302 | 303 | 307 | 308:
return "Redirect"
case 400 | 401 | 403 | 404:
return "Client error"
case 500 | 502 | 503 | 504:
return "Server error"
case _:
return "Unknown status"
# Test with various codes
print(categorize_http_status(200)) # Output: Success
print(categorize_http_status(302)) # Output: Redirect
print(categorize_http_status(404)) # Output: Client error
print(categorize_http_status(500)) # Output: Server error
print(categorize_http_status(999)) # Output: Unknown status
The pattern 200 | 201 | 202 | 204 matches if the code is any of those values. This is far cleaner than four separate cases. Or-patterns work with any pattern type: literals, captures, sequences, mappings, and classes.
Wildcard Pattern for Ignoring Values
The _ wildcard pattern matches anything without binding a variable. Use it when you don't need the matched value, making it clear you're ignoring it intentionally.
def process_with_wildcards(data: tuple) -> str:
"""Process tuples, ignoring unneeded values."""
match data:
case (command, _):
# Ignore the second element
return f"Command: {command}"
case (command, arg, _):
# Ignore the third element
return f"Command {command} with arg {arg}"
case (command, *_):
# Match command and ignore remaining elements
return f"Multi-arg command: {command}"
case _:
# Default: ignore everything
return "Invalid input"
# Test with tuples
print(process_with_wildcards(("help", "topic"))) # Output: Command: help
print(process_with_wildcards(("set", "key", "value"))) # Output: Command set with arg key
print(process_with_wildcards(("echo", "hello", "world", "!"))) # Output: Multi-arg command: echo
The _ wildcard makes it explicit that you're not using a value. Compare case (command, _): to case (command, x): where x is never referenced—the underscore is clearer and avoids the "unused variable" pattern.
Combining Or-Patterns with Captures
Or-patterns can bind variables if all alternatives capture the same names. This is useful for matching multiple patterns that extract the same structure.
def process_event(event: dict) -> str:
"""Process different event types with or-patterns."""
match event:
case {"type": "login" | "logout", "user_id": user_id, "timestamp": ts}:
action = "logged in" if event.get("type") == "login" else "logged out"
return f"User {user_id} {action} at {ts}"
case {
"type": "error" | "warning" | "info",
"message": msg,
"level": level
}:
return f"[{level.upper()}] {msg}"
case {"type": "unknown", **rest}:
return f"Unknown event: {rest}"
case _:
return "Invalid event"
# Test with events
print(process_event({"type": "login", "user_id": 42, "timestamp": 1000}))
# Output: User 42 logged in at 1000
print(process_event({"type": "error", "message": "Connection lost", "level": "critical"}))
# Output: [CRITICAL] Connection lost
The pattern {"type": "login" | "logout", "user_id": user_id, "timestamp": ts} matches events where "type" is either "login" or "logout", capturing both user_id and ts. All patterns in the or-pattern extract the same variables.
Combining Different Pattern Types with Or
Or-patterns work across different pattern types: literals, captures, sequences, and classes. This enables powerful, flexible matching.
def parse_input(value) -> str:
"""Parse different input types using or-patterns."""
match value:
case 0 | "none" | None:
# Match different representations of "nothing"
return "Null/empty input"
case int(n) | float(n) if n > 0:
# Match positive numbers of any numeric type
return f"Positive number: {n}"
case [x, y] | (x, y):
# Match either a 2-element list or tuple
return f"Pair: ({x}, {y})"
case {"name": name} | name if isinstance(name, str):
# Match a dict with name key OR a string
return f"Name: {name}"
case _:
return "Unknown input"
# Test with various inputs
print(parse_input(0)) # Output: Null/empty input
print(parse_input("none")) # Output: Null/empty input
print(parse_input(None)) # Output: Null/empty input
print(parse_input(42)) # Output: Positive number: 42
print(parse_input(3.14)) # Output: Positive number: 3.14
print(parse_input([1, 2])) # Output: Pair: (1, 2)
print(parse_input(("a", "b"))) # Output: Pair: ('a', 'b')
The or-pattern 0 | "none" | None groups three representations of emptiness. The pattern [x, y] | (x, y) matches either a 2-element list or tuple, both binding x and y.
Guard Clauses with Or-Patterns
Guards apply to the entire or-pattern—if the guard is false, all alternatives fail and the next case is tried.
def classify_action(action: dict) -> str:
"""Classify actions using or-patterns with guards."""
match action:
case {"op": "create" | "update" | "delete", "resource": res} if res in ["user", "post", "comment"]:
verb = "Creating" if action["op"] == "create" else "Updating" if action["op"] == "update" else "Deleting"
return f"{verb} {res}"
case {"op": "create" | "update" | "delete", "resource": res}:
return f"Invalid resource: {res}"
case {"op": op}:
return f"Unknown operation: {op}"
case _:
return "Invalid action"
# Test with actions
print(classify_action({"op": "create", "resource": "user"}))
# Output: Creating user
print(classify_action({"op": "update", "resource": "invalid"}))
# Output: Invalid resource: invalid
print(classify_action({"op": "read", "resource": "user"}))
# Output: Unknown operation: read
The guard if res in ["user", "post", "comment"] applies to all alternatives in "op": "create" | "update" | "delete". If the guard is false, all three alternatives fail.
Nested Or-Patterns and Complex Structures
Or-patterns nest arbitrarily deep, combining with sequences, mappings, and classes for complex matching logic.
def route_message(msg: dict) -> str:
"""Route messages based on complex or-patterns."""
match msg:
case {
"type": "request" | "query",
"path": "/api/users" | "/api/posts" | "/api/comments",
"method": "GET" | "HEAD"
}:
path = msg["path"]
return f"Retrieve: {path}"
case {
"type": "request" | "command",
"path": path,
"method": "POST" | "PUT" | "PATCH"
} if path.startswith("/api/"):
return f"Modify: {path}"
case {
"type": "request",
"path": path,
"method": "DELETE"
}:
return f"Delete: {path}"
case _:
return "Unhandled message"
# Test routing
print(route_message({"type": "request", "path": "/api/users", "method": "GET"}))
# Output: Retrieve: /api/users
print(route_message({"type": "command", "path": "/api/posts", "method": "POST"}))
# Output: Modify: /api/posts
print(route_message({"type": "request", "path": "/static/app.js", "method": "GET"}))
# Output: Unhandled message
Multiple or-patterns at different levels combine: "type": "request" | "query" OR "path": "/api/users" | "/api/posts" OR "method": "GET" | "HEAD". All conditions must be satisfied for the case to match.
Comparison Table: When to Use Or-Patterns vs. Separate Cases
| Scenario | Or-Pattern | Separate Cases |
|---|---|---|
| Match same action for multiple values | case 1 | 2 | 3: | case 1: ... case 2: ... case 3: ... |
| Extract different structures | Multiple or-patterns: case [x, y] | (x, y): | Separate cases per structure |
| One handler for many conditions | Or-pattern in one case | Separate cases (duplication) |
| Different logic per value | N/A; use separate cases | case 1: ... case 2: ... |
Key Takeaways
- Or-patterns combine multiple patterns with
|, matching if any alternative succeeds. - Wildcard
_matches anything without binding a variable, making it clear you're ignoring a value. - All patterns in an or-pattern must bind the same variables (or none).
- Guards apply to the entire or-pattern—if false, all alternatives fail.
- Or-patterns work across all pattern types: literals, captures, sequences, mappings, and classes.
- Or-patterns reduce code duplication by grouping related cases into one.
- Nested or-patterns enable complex, multi-level matching logic.
Frequently Asked Questions
Can or-patterns have different variable names?
No. All alternatives in an or-pattern must bind the same variable names, or bind no variables at all. If alternatives bind different variables, Python raises a SyntaxError. This prevents ambiguity about which variable is bound.
What's the difference between case _: and case default:?
_ is the wildcard and matches anything. case default: would try to match a variable named default, not a default case. Always use _ for the default case.
Can I have an or-pattern as the default case?
Yes: case 1 | 2 | 3 | _: is valid, but it's redundant—_ already matches anything. The or-pattern never adds functionality once _ is included.
Do or-patterns short-circuit?
No, all alternatives are evaluated until one matches. There's no short-circuit evaluation like in or expressions. Pattern matching is exhaustive.
Can I use or-patterns with class patterns?
Yes: case User(name=n) | Admin(name=n): matches either a User or Admin instance and binds the name. Both classes must have a name attribute for this to work consistently.