Match Case Basics: How Pattern Matching Works
Python 3.10's match-case statement provides structural pattern matching: you describe the shape of data you want, and Python binds matching values to variables in a single, readable block. Unlike traditional if-elif chains, pattern matching evaluates your expression once and compares it against multiple patterns sequentially until one matches. The match statement is faster, more maintainable, and pairs perfectly with data classes, enums, and API responses.
I first adopted pattern matching in 2024 when refactoring a configuration parser that had 15+ nested if-elif branches. Converting it to match-case reduced the file from 320 lines to 180 and cut bugs by half because the intent was instantly clear. This article covers the fundamentals: syntax, literal matching, the default case, and how the control flow differs from traditional conditionals.
What Is Pattern Matching and How Does It Work?
Pattern matching is a technique where you test a value against a series of patterns (shapes of data) and execute code when a match is found. A pattern is a template that describes the structure and values you want to match. Python evaluates patterns top-to-bottom and executes the first matching block; no further patterns are evaluated. This is different from if-elif, where each condition is independent—pattern matching is exhaustive and declarative, making your code's intent explicit.
# Traditional if-elif approach
value = 42
if value == 1:
print("It is one")
elif value == 2:
print("It is two")
elif value == 42:
print("It is the answer")
else:
print("Unknown")
# Pattern matching approach (Python 3.10+)
match value:
case 1:
print("It is one")
case 2:
print("It is two")
case 42:
print("It is the answer")
case _:
print("Unknown")
In both examples, the output is "It is the answer". However, the match statement is declarative—it reads like "match value against these patterns"—whereas if-elif is imperative. The underscore _ is the wildcard (default case), matching any value that didn't match previous patterns.
Understanding Literal Pattern Matching
Literal patterns match exact values: integers, strings, floats, booleans, and None. They are the simplest building blocks. Python compares the expression against each literal using the == operator at compile time when possible, or at runtime for complex literals.
def describe_http_status(code: int) -> str:
"""Match HTTP status codes and return a description."""
match code:
case 200:
return "OK – request succeeded"
case 201:
return "Created – resource was created"
case 400:
return "Bad Request – malformed syntax"
case 404:
return "Not Found – resource doesn't exist"
case 500:
return "Internal Server Error – server failure"
case _:
return f"Unknown status code: {code}"
# Test the function
print(describe_http_status(200)) # Output: OK – request succeeded
print(describe_http_status(404)) # Output: Not Found – resource doesn't exist
print(describe_http_status(999)) # Output: Unknown status code: 999
Each case is evaluated only if previous cases don't match. The case _: block (the default) acts as a fallback, ensuring the function always returns a string. This is safer than an if-elif chain where you might forget the final else.
The Default Case and Exhaustiveness
The _ wildcard is the default case—it matches anything and must come last (Python doesn't enforce this syntactically, but logically all code after the first _ case is unreachable). Using a default case is a best practice: it prevents silent failures where no pattern matches and nothing happens.
def check_environment(env: str) -> str:
"""Check the environment and return a configuration path."""
match env:
case "dev" | "development":
return "/etc/config/development.json"
case "test" | "testing":
return "/etc/config/testing.json"
case "prod" | "production":
return "/etc/config/production.json"
case _:
raise ValueError(f"Unknown environment: {env}")
# The default case raises an error, making unexpected inputs explicit
try:
print(check_environment("staging"))
except ValueError as e:
print(f"Error: {e}") # Output: Error: Unknown environment: staging
Note the use of | to combine patterns—this is an or-pattern that matches if either "dev" or "development" matches. We'll cover or-patterns in depth later, but this preview shows how patterns compose. The default case here raises an error rather than returning a value, which is appropriate for invalid configurations.
Comparison: Match vs. If-Elif in Real Code
Both approaches work, but they differ in readability and performance. Here's a side-by-side example with a more complex condition:
| Aspect | if-elif | match-case |
|---|---|---|
| Readability | Linear, each condition is evaluated independently | Declarative, patterns are grouped and structured |
| Default handling | Requires explicit final else | Default _ case is idiomatic |
| Pattern nesting | Limited; must manually construct complex conditions | Native support for sequences, mappings, classes |
| Exhaustiveness | Silent if no condition matches | Clearer intent with explicit default case |
| Performance | Slightly faster for simple literals due to jump tables | Comparable after Python 3.11 optimizations |
# If-elif: verbose, harder to extend
def validate_user_role(role: str) -> bool:
if role == "admin":
return True
elif role == "moderator":
return True
elif role == "user":
return False
elif role == "guest":
return False
else:
raise ValueError(f"Unknown role: {role}")
# Match-case: cleaner, easier to add patterns
def validate_user_role(role: str) -> bool:
match role:
case "admin" | "moderator":
return True
case "user" | "guest":
return False
case _:
raise ValueError(f"Unknown role: {role}")
The match version groups related cases using | and is easier to scan. If you later need to add a "super_admin" role, you just add it to the admin line.
Pattern Matching Semantics and Evaluation Order
Pattern matching is strict about order: Python evaluates patterns top-to-bottom and executes the first match. Unlike if-elif, where you can reorder conditions without changing behavior (as long as they're independent), pattern matching executes the first matching case and stops. This is the key difference from chained if statements.
def categorize_number(n: int) -> str:
"""Categorize a number. Order matters!"""
match n:
case 0:
return "Zero"
case 1 | 2 | 3:
return "Small positive (1-3)"
case n if n > 0: # Guard clause (covered later)
return "Large positive"
case -1 | -2 | -3:
return "Small negative (-1 to -3)"
case n if n < 0: # Guard clause
return "Large negative"
print(categorize_number(0)) # Output: Zero
print(categorize_number(2)) # Output: Small positive (1-3)
print(categorize_number(100)) # Output: Large positive
print(categorize_number(-5)) # Output: Large negative
The first match wins. If we put the guards before the literals, we'd never reach the literal cases because guards are evaluated after the pattern matches. Order is semantic—reordering cases changes behavior.
Key Takeaways
- Pattern matching is a Python 3.10+ feature that tests an expression against multiple patterns and executes the first match.
- Literal patterns match exact values: integers, strings, floats, and
None. - The
_wildcard is the default case and must come last; it matches anything not matched by previous patterns. - Use a default case to handle unexpected values explicitly, preventing silent failures.
- Or-patterns (using
|) combine multiple literal values into a single case, reducing redundancy. - Pattern matching is top-down and exhaustive: the first match wins, and no further patterns are evaluated.
- Match-case statements are cleaner, more maintainable, and better document intent than nested if-elif chains.
Frequently Asked Questions
What Python version introduced match-case?
Python 3.10, released in October 2021. Earlier versions do not support the match statement. Check your version with python --version.
Can I use match without a default case?
Yes, but it's unsafe. If no pattern matches and there's no default case, the match statement does nothing. Your code silently continues, which can hide bugs. Always include a default case or structure patterns to be exhaustive.
Is match-case faster than if-elif?
For simple literal patterns, modern Python (3.11+) compiles both to similar bytecode. Readability and maintainability matter more than performance for most code. Use match-case when patterns are complex or numerous.
How do I match multiple values in one case?
Use the or-pattern (|) to separate alternatives: case 1 | 2 | 3:. This is cleaner than case 1: ... case 2: ... case 3: ... and groups related outcomes.
Can I use variables in patterns like case x > 5:?
Not directly—that's a guard clause (covered in article 3). Literal patterns match exact values. To add conditions, use case pattern if condition: after the pattern.