15.6 Guard Clauses in match/case
Guard clauses, introduced in Python 3.10 alongside structural pattern matching, are a powerful mechanism for adding conditional logic directly within a case statement. They allow you to refine a pattern match by requiring that an additional arbitrary expression evaluates to True for the case to be considered a match. This moves beyond the structural decomposition of the subject and into the realm of evaluating its content or state, enabling far more expressive and precise control flow.
Syntax and Basic Usage of Guard Clauses
A guard clause is appended to a pattern using the if keyword. The syntax is case <pattern> if <expression>:. The expression can be any valid Python expression. Crucially, the guard is only evaluated after the pattern has successfully matched. This order of operations is vital: the pattern first destructures the data, binding names to its parts, and then the guard can use those bound names in its condition.
def categorize_number(value):
match value:
case int() | float() if value > 0:
print(f"{value} is a positive number.")
case int() | float() if value < 0:
print(f"{value} is a negative number.")
case int() | float():
print("The number is zero.")
case _:
print(f"{value} is not a number.")
categorize_number(42) # Output: 42 is a positive number.
categorize_number(-3.14) # Output: -3.14 is a negative number.
categorize_number(0) # Output: The number is zero.
categorize_number("text") # Output: text is not a number.
In this example, the first two patterns are identical in their structural matching (they both accept int or float), but they are disambiguated by their guard clauses, which inspect the bound value.
Why Order of Evaluation Matters
The interpreter processes a match statement by checking each case in sequence. It first attempts to match the pattern. If that fails, it moves to the next case without evaluating the guard. Only if the pattern matches does it then evaluate the guard expression. If the guard evaluates to True, the case block is executed. If the guard evaluates to False, the match continues to the next case as if the pattern had failed. This has significant implications for design and performance.
def check_data(data):
match data:
case (x, y) if x > y: # Pattern matches first, then guard checks
print(f"First element ({x}) is larger than second ({y}).")
case (x, y): # This pattern also matches, but only if the guard above was False
print(f"First element ({x}) is not larger than second ({y}).")
case _:
print("Not a two-element sequence.")
check_data((10, 5)) # Output: First element (10) is larger than second (5).
check_data((5, 10)) # Output: First element (5) is not larger than second (10).
Common Pitfalls and Best Practices
A frequent pitfall is placing a more general case before a more specific one, causing the specific case to never be reached. This is especially dangerous with guards.
# INCORRECT ORDERING - The second case will never run
match value:
case str() if value.startswith("error"): # This matches any string starting with "error"
print("General error")
case str("error_404"): # This more specific pattern is never reached
print("404 Not Found")
# CORRECT ORDERING - More specific first
match value:
case str("error_404"):
print("404 Not Found")
case str() if value.startswith("error"):
print("General error")
Best Practice: Always order your cases from most specific to most general. Cases with concrete values should come before cases with type checks and guards.
Another pitfall involves the scope of names bound in patterns. A name bound in a pattern is available throughout the entire match block, but its value is only set if the pattern matched. Using a name from a failed pattern in a subsequent guard is a error.
# This will cause a NameError if the first pattern fails
match data:
case {"status": 200, "data": payload}:
print("Success")
case _ if payload: # 'payload' is not defined here if first case didn't match!
print("Payload exists but status was not 200")
Advanced Use Cases: Combining with Class Patterns
Guards become exceptionally powerful when combined with class patterns, allowing you to match on an object’s structure and then interrogate its state.
class Customer:
def __init__(self, name, tier):
self.name = name
self.tier = tier
def handle_customer(customer):
match customer:
case Customer(tier="premium") if customer.name.startswith("VIP_"):
print(f"Handling VIP premium customer: {customer.name}")
case Customer(tier="premium"):
print(f"Handling standard premium customer: {customer.name}")
case Customer():
print(f"Handling standard customer: {customer.name}")
# Example usage
vip = Customer("VIP_Alice", "premium")
prem = Customer("Bob", "premium")
standard = Customer("Charlie", "standard")
handle_customer(vip) # Output: Handling VIP premium customer: VIP_Alice
handle_customer(prem) # Output: Handling standard premium customer: Bob
handle_customer(standard) # Output: Handling standard customer: Charlie
Here, the first case matches any Customer object with a tier attribute of "premium" and whose name attribute (accessed via the bound customer variable) starts with "VIP_". This level of conditional logic would be cumbersome to write using only if/elif chains outside the match statement.