13. Making Choices with match and case (Structural Pattern Matching)
When your program needs to make decisions based on multiple possible values or patterns, you've already learned to use if-elif-else chains from Chapter 8. Python 3.10 introduced a powerful alternative called structural pattern matching using match and case statements. This feature provides a cleaner, more expressive way to handle complex decision-making scenarios.
Pattern matching goes beyond simple value comparisons. It allows you to match against the structure and shape of data, extract values from complex objects, and express multi-way decisions in a more readable form. While if-elif-else chains work perfectly well for many situations, match-case statements shine when you're dealing with multiple distinct cases, especially when working with structured data.
13.1) Introducing match and case Statements (Building on if-elif from Chapter 8)
13.1.1) The Basic Structure of match-case
A match statement examines a value (called the subject) and compares it against one or more patterns defined in case clauses. When a pattern matches, Python executes the code block associated with that case.
Here's the basic structure:
match subject:
case pattern1:
# Code to execute if pattern1 matches
case pattern2:
# Code to execute if pattern2 matches
case pattern3:
# Code to execute if pattern3 matchesLet's start with a simple example that demonstrates the fundamental concept:
# Simple HTTP status code handler
status_code = 404
match status_code:
case 200:
print("Success: Request completed")
case 404:
print("Error: Page not found")
case 500:
print("Error: Server error")Output:
Error: Page not foundIn this example, the match statement examines status_code (the subject). Python checks each case pattern in order. When it finds that status_code equals 404, it executes the corresponding code block and then exits the match statement. The remaining cases are not checked.
13.1.2) How match-case Differs from if-elif-else
You might wonder: "Couldn't I write this with if-elif-else?" Yes, you could:
status_code = 404
if status_code == 200:
print("Success: Request completed")
elif status_code == 404:
print("Error: Page not found")
elif status_code == 500:
print("Error: Server error")Output:
Error: Page not foundBoth versions produce the same result. However, match-case offers several advantages:
- Clearer intent: The
matchstatement explicitly shows you're checking one value against multiple possibilities - Less repetition: You don't repeat the variable name in every comparison
- More powerful patterns: As we'll see,
match-casecan do much more than simple equality checks - Better readability: For complex decision trees,
match-caseis often easier to understand
13.1.3) When No Pattern Matches
What happens if none of the patterns match? The match statement simply completes without executing any case block:
# User role checker
user_role = "guest"
match user_role:
case "admin":
print("Full system access granted")
case "moderator":
print("Content management access granted")
case "editor":
print("Editing access granted")
print("Role check complete")Output:
Role check completeSince "guest" doesn't match any of the patterns, no case block executes. The program continues with the code after the match statement. This behavior is important to understand—unlike if-elif-else chains where you can add a final else clause to catch all other cases, a basic match statement without a catch-all pattern will silently do nothing if no pattern matches.
13.1.4) Practical Example: Menu Selection System
Let's build a more complete example that demonstrates the clarity of match-case for handling user choices:
# Restaurant order system
menu_choice = 3
match menu_choice:
case 1:
item = "Caesar Salad"
price = 8.99
print(f"You ordered: {item} - ${price}")
case 2:
item = "Grilled Chicken"
price = 14.99
print(f"You ordered: {item} - ${price}")
case 3:
item = "Vegetable Pasta"
price = 12.99
print(f"You ordered: {item} - ${price}")
case 4:
item = "Chocolate Cake"
price = 6.99
print(f"You ordered: {item} - ${price}")
print("Order submitted to kitchen")Output:
You ordered: Vegetable Pasta - $12.99
Order submitted to kitchenThis example shows how each case can contain multiple statements. When menu_choice matches 3, Python executes all three lines in that case block: assigning item, assigning price, and printing the order confirmation.
13.2) Using the _ Wildcard, Literal Patterns, and Multiple Patterns
13.2.1) The Wildcard Pattern: Catching Everything Else
The underscore _ is a special pattern that matches anything. It's typically used as the last case to handle all values that didn't match previous patterns—similar to a final else clause in an if-elif-else chain:
# HTTP status code handler with default case
status_code = 403
match status_code:
case 200:
print("Success: Request completed")
case 404:
print("Error: Page not found")
case 500:
print("Error: Server error")
case _:
print(f"Unhandled status code: {status_code}")Output:
Unhandled status code: 403The _ pattern acts as a catch-all. Since 403 doesn't match any of the specific cases, the wildcard pattern matches and executes its block. The wildcard pattern will match any value, so it should always be placed last—any cases after it would never execute.
Here's why the wildcard is useful in practice:
# Day of week scheduler
day = "Saturday"
match day:
case "Monday":
print("Team meeting at 9 AM")
case "Wednesday":
print("Project review at 2 PM")
case "Friday":
print("Weekly report due")
case _:
print(f"{day}: No scheduled events")Output:
Saturday: No scheduled eventsWithout the wildcard pattern, if day were "Saturday", "Sunday", or any other value, the match statement would complete silently with no output. The wildcard ensures you handle unexpected or unspecified cases gracefully.
13.2.2) Literal Patterns: Matching Specific Values
Literal patterns match exact values. We've been using them already—numbers, strings, and boolean values are all literal patterns:
# Traffic light controller
light_color = "yellow"
match light_color:
case "green":
print("Go")
case "yellow":
print("Caution: Light changing soon")
case "red":
print("Stop")
case _:
print("Invalid light color")Output:
Caution: Light changing soonYou can use literal patterns of different types, and match compares both the value and its type:
# Configuration validator (using different literal types)
setting_value = True
match setting_value:
case True: # boolean literal
print("Feature enabled")
case False: # boolean literal
print("Feature disabled")
case None: # None literal
print("Feature not configured")
case 0: # integer literal
print("Feature explicitly turned off")
case "auto": # string literal
print("Feature set to automatic mode")
case _:
print("Invalid configuration value")Output:
Feature enabledLiteral patterns work with integers, floats, strings, booleans, and None. Python checks for equality using the same rules as the == operator.
13.2.3) Multiple Patterns with the OR Operator
Sometimes you want to execute the same code for several different values. You can combine multiple patterns using the | (pipe) operator, which means "or":
# Weekend detector
day = "Saturday"
match day:
case "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday":
print("It's a weekday - time to work!")
case "Saturday" | "Sunday":
print("It's the weekend - time to relax!")
case _:
print("Invalid day name")Output:
It's the weekend - time to relax!The | operator lets you specify multiple patterns that should trigger the same action. If the subject matches any of the patterns separated by |, that case executes. This is much cleaner than writing separate cases with identical code blocks.
You can mix different types of patterns with |:
# Input validator for yes/no questions
response = "yes"
match response:
case True | "yes":
print("You confirmed the action")
case False | "no":
print("You cancelled the action")
case _:
print("Please answer yes or no")Output:
You confirmed the action13.2.4) Capturing Which Alternative Matched with as
When using multiple patterns with |, you might want to know which specific value matched. You can use the as keyword to capture the matched value:
# Status code handler with grouped responses
status = 201
match status:
case 200 | 201 | 202 | 204 as success_code:
print(f"Success: {success_code}")
case 400 | 401 | 403 | 404 as client_error:
print(f"Client error: {client_error}")
case 500 | 502 | 503 as server_error:
print(f"Server error: {server_error}")
case _:
print("Unknown status code")Output:
Success: 201The as keyword creates a binding variable that captures whichever alternative matched. In this example, success_code is bound to 201 because that's the specific value that matched from the alternatives 200 | 201 | 202 | 204.
Here's another example showing how this is useful for logging:
# Log level processor
log_level = "WARN"
match log_level:
case "DEBUG" | "TRACE" as level:
print(f"Verbose logging: {level}")
print("Detailed diagnostic information will be recorded")
case "INFO" | "NOTICE" as level:
print(f"Informational: {level}")
print("Normal operation messages will be recorded")
case "WARN" | "WARNING" as level:
print(f"Warning level: {level}")
print("Potential issues detected")
case "ERROR" | "FATAL" | "CRITICAL" as level:
print(f"Error level: {level}")
print("Immediate attention required")
case _:
print("Unknown log level")Output:
Warning level: WARN
Potential issues detected13.3) Extracting Values with Binding Variables
13.3.1) What Are Binding Variables?
So far, we've matched against literal values. But pattern matching becomes truly powerful when you can capture or extract parts of the data you're matching. A binding variable (also called a capture pattern) is a name in a pattern that captures the matched value and makes it available in the case block.
Here's a simple example:
# Simple value capture
command = "save"
match command:
case "quit":
print("Exiting program")
case action: # This is a binding variable
print(f"Executing action: {action}")Output:
Executing action: saveThe pattern action is a binding variable. It matches any value (like the wildcard _), but unlike _, it captures that value and assigns it to the name action. Inside the case block, you can use action to refer to the matched value.
Important distinction: A binding variable matches anything, just like _. The difference is that _ discards the value, while a binding variable captures it for use in the case block.
13.3.2) Binding Variables vs Wildcards
Let's compare binding variables and wildcards directly:
# Using wildcard - value is not captured
status = 403
match status:
case 200:
print("Success")
case _:
print("Some other status code") # Can't access the actual valueOutput:
Some other status codeNow with a binding variable:
# Using binding variable - value is captured
status = 403
match status:
case 200:
print("Success")
case code: # Binding variable captures the value
print(f"Status code {code} received")Output:
Status code 403 receivedThe binding variable code captures the value 403, making it available inside the case block. This is useful when you need to work with the actual value that didn't match your specific patterns.
13.3.3) Matching Tuple Patterns and Extracting Components
Pattern matching becomes especially powerful with structured data like tuples. You can match the shape of a tuple and extract its components simultaneously. Although we'll study tuples in detail in Chapter 15, this example focuses only on how tuple patterns work in match statements.
# Coordinate system - matching a tuple pattern
point = (3, 7)
match point:
case (0, 0):
print("Origin point")
case (0, y): # Matches any point on the y-axis
print(f"On y-axis at y={y}")
case (x, 0): # Matches any point on the x-axis
print(f"On x-axis at x={x}")
case (x, y): # Matches any other point
print(f"Point at coordinates ({x}, {y})")Output:
Point at coordinates (3, 7)Let's break down what's happening:
- The subject
pointis the tuple(3, 7) - Python checks each case pattern in order
- The first three patterns don't match because they require the value
0in a specific position, and the tuple(3, 7)has no element equal to0 - The pattern
(x, y)matches because it's a tuple of two elements - Python binds
xto3andyto7 - The case block executes with these captured values
Here's another example showing different tuple patterns:
# RGB color analyzer
color = (255, 0, 0)
match color:
case (0, 0, 0):
print("Black")
case (255, 255, 255):
print("White")
case (r, 0, 0): # Pure red with varying intensity
print(f"Pure red with intensity {r}")
case (0, g, 0): # Pure green
print(f"Pure green with intensity {g}")
case (0, 0, b): # Pure blue
print(f"Pure blue with intensity {b}")
case (r, g, b): # Any other color
print(f"RGB color: red={r}, green={g}, blue={b}")Output:
Pure red with intensity 255This input matches the pattern (r, 0, 0) because the tuple has three elements, the last two are 0, and the first value is bound to r.
13.3.4) Matching List Patterns
You can also match list patterns and extract elements. We'll cover lists in detail in Chapter 14; for now, this example focuses on how list patterns work in match statements:
# Command with arguments
command = ["move", "north", "5"]
match command:
case ["quit"]:
print("Exiting game")
case ["look"]:
print("You look around the room")
case ["move", direction]:
print(f"Moving {direction}")
case ["move", direction, distance]:
print(f"Moving {direction} for {distance} steps")
case _:
print("Unknown command")Output:
Moving north for 5 stepsThe pattern ["move", direction, distance] matches a three-element list where the first element is "move". It captures the second element as direction and the third as distance.
Here's a practical example with varying list lengths:
# Shopping cart item processor
item = ["laptop", 999.99, 2]
match item:
case [name]: # Item with just a name
print(f"Item: {name} (no price or quantity specified)")
case [name, price]: # Item with name and price
print(f"Item: {name}, Price: ${price}, Quantity: 1 (default)")
case [name, price, quantity]: # Complete item information
total = price * quantity
print(f"Item: {name}, Price: ${price}, Quantity: {quantity}")
print(f"Subtotal: ${total}")
case _:
print("Invalid item format")Output:
Item: laptop, Price: $999.99, Quantity: 2
Subtotal: $1999.98The case [name, price, quantity] executes because the list has exactly three elements, and each element is bound to its corresponding variable.
13.3.5) Matching Dictionary Patterns
Pattern matching works with dictionaries too, allowing you to match specific keys and extract their values. Although we'll study dictionaries in detail in Chapter 16, this section focuses only on how dictionary patterns work in match statements.
# User profile processor
user = {"name": "Alice", "role": "admin", "active": True}
match user:
case {"role": "admin", "active": True}:
print("Active administrator - full access granted")
case {"role": "admin", "active": False}:
print("Inactive administrator - access suspended")
case {"role": role, "active": True}: # Capture the role value
print(f"Active user with role: {role}")
case {"role": role, "active": False}:
print(f"Inactive user with role: {role}")
case _:
print("Invalid user profile")Output:
Active administrator - full access grantedThe case {"role": "admin", "active": True} executes because dictionary patterns require matching key–value pairs, and this exact match is checked before more general patterns.
Dictionary patterns are flexible—they match if the specified keys exist with the specified values, even if the dictionary has additional keys:
# API response handler
response = {"status": "success", "data": {"id": 123, "name": "Product"}, "timestamp": "2025-12-17"}
match response:
case {"status": "error", "message": msg}:
print(f"Error occurred: {msg}")
case {"status": "success", "data": data}:
print(f"Success! Data received: {data}")
case _:
print("Unknown response format")Output:
Success! Data received: {'id': 123, 'name': 'Product'}The pattern {"status": "success", "data": data} matches even though the dictionary has an additional "timestamp" key. The pattern only requires that the specified keys exist with the specified values (or patterns).
13.3.6) Combining Literals and Binding Variables
You can mix literal patterns and binding variables to create sophisticated matching logic: Unlike the earlier tuple examples that focused on matching structure and position, this example shows how literal values and binding variables can be combined to implement real-world decision logic.
# HTTP request router
request = ("GET", "/api/users", 42)
match request:
case ("GET", "/", None):
print("Homepage request")
case ("GET", path, None):
print(f"GET request for: {path}")
case ("POST", path, data):
print(f"POST request to {path} with data: {data}")
case ("GET", path, user_id):
print(f"GET request for {path} with user ID: {user_id}")
case _:
print("Unsupported request type")Output:
GET request for /api/users with user ID: 42This example shows how you can match specific values (like "GET") while capturing others (like path and user_id) in the same pattern.
13.3.7) Practical Example: Event Handler
Let's build a complete example that demonstrates the power of binding variables:
# Game event handler
event = ("player_move", {"x": 10, "y": 5, "speed": 2})
match event:
case ("player_move", {"x": x, "y": y}):
print(f"Player moved to position ({x}, {y})")
case ("player_attack", {"target": target, "damage": damage}):
print(f"Player attacked {target} for {damage} damage")
case ("item_pickup", {"item": item_name}):
print(f"Player picked up: {item_name}")
case ("game_over", {"score": final_score}):
print(f"Game ended. Final score: {final_score}")
case (event_type, data):
print(f"Unknown event type: {event_type}")
print(f"Event data: {data}")Output:
Player moved to position (10, 5)This event handler matches tuples containing an event type and a dictionary of event data. It extracts specific values from the dictionary based on the event type, making it easy to process different kinds of events with clean, readable code.
13.4) Adding Extra Conditions with the if Guard
13.4.1) What Are Guards?
Sometimes you need to match a pattern and check an additional condition. An if guard is an extra condition you can add to a case pattern using the if keyword. The case only matches if both the pattern matches and the guard condition is true.
Here's the syntax:
match subject:
case pattern if condition:
# Code executes only if pattern matches AND condition is trueLet's see a simple example:
# Age-based access control
age = 16
match age:
case age if age >= 18:
print("Adult - full access granted")
case age if age >= 13:
print("Teen - limited access granted")
case age if age >= 0:
print("Child - parental supervision required")
case _:
print("Invalid age")Output:
Teen - limited access grantedIn this example, the binding variable age captures the value, and the guard if age >= 13 adds an additional condition. The case matches only if the value is 13 or greater. Since age is 16, the second case matches and executes.
13.4.2) How Guards Are Evaluated
Understanding the evaluation order is important. Here's a detailed visualization showing how guards interact with pattern matching:
Python first checks if the pattern matches. Only if the pattern matches does Python evaluate the guard condition. If the guard is false, Python moves to the next case—even though the pattern matched.
Here's an example that demonstrates this:
# Temperature warning system
temperature = 25
match temperature:
case temp if temp > 35:
print(f"Extreme heat warning: {temp}°C")
case temp if temp > 30:
print(f"High temperature alert: {temp}°C")
case temp if temp > 20:
print(f"Comfortable temperature: {temp}°C")
case temp if temp > 10:
print(f"Cool temperature: {temp}°C")
case temp:
print(f"Cold temperature: {temp}°C")Output:
Comfortable temperature: 25°CEach case uses the binding variable temp to capture the temperature value, then applies a guard to check if it falls within a specific range. The cases are checked in order, so the first matching case with a true guard executes.
13.4.3) Guards with Literal Patterns
You can combine guards with literal patterns to create more specific matching:
# Discount calculator based on item type and quantity
item = ("book", 5)
match item:
case ("book", quantity) if quantity >= 10:
discount = 0.20 # 20% discount for 10+ books
print(f"Bulk book order: {quantity} books, {discount*100}% discount")
case ("book", quantity) if quantity >= 5:
discount = 0.10 # 10% discount for 5-9 books
print(f"Book order: {quantity} books, {discount*100}% discount")
case ("book", quantity):
discount = 0.0 # No discount for fewer than 5 books
print(f"Book order: {quantity} books, no discount")
case (item_type, quantity):
print(f"Order: {quantity} {item_type}(s)")Output:
Book order: 5 books, 10.0% discountThe pattern ("book", quantity) matches a tuple where the first element is "book". The guard if quantity >= 5 adds the condition that the quantity must be at least 5.
13.4.4) Guards with Complex Conditions
Guards can use any boolean expression, including complex conditions with and, or, and not:
# Student grade evaluator with attendance consideration
student = {"name": "Bob", "grade": 85, "attendance": 75}
match student:
case {"grade": g, "attendance": a} if g >= 90 and a >= 90:
status = "Excellent"
print(f"Grade: {g}, Attendance: {a}% - Status: {status}")
case {"grade": g, "attendance": a} if g >= 80 and a >= 80:
status = "Good"
print(f"Grade: {g}, Attendance: {a}% - Status: {status}")
case {"grade": g, "attendance": a} if g >= 70 and a >= 70:
status = "Satisfactory"
print(f"Grade: {g}, Attendance: {a}% - Status: {status}")
case {"grade": g, "attendance": a} if g >= 60 or a >= 60:
status = "Needs Improvement"
print(f"Grade: {g}, Attendance: {a}% - Status: {status}")
case _:
print("Failing - immediate intervention required")Output:
Grade: 85, Attendance: 75% - Status: SatisfactoryThe guard if g >= 70 and a >= 70 requires both the grade and attendance to be at least 70. Since Bob has a grade of 85 and attendance of 75%, this case matches.
13.4.5) Practical Example: User Authentication System
Let's build a complete example that uses guards to implement a realistic authentication system:
# User authentication with role-based access
user = {"username": "alice", "role": "admin", "active": True, "login_attempts": 0}
match user:
case {"active": False}:
print("Account suspended - contact administrator")
case {"login_attempts": attempts} if attempts >= 3:
print("Account locked due to too many failed login attempts")
case {"role": "admin", "active": True}:
print("Admin access granted - full system privileges")
case {"role": "moderator", "active": True}:
print("Moderator access granted - content management privileges")
case {"role": role, "active": True} if role in ["editor", "author"]:
print(f"{role.capitalize()} access granted - content creation privileges")
case {"role": "user", "active": True}:
print("User access granted - basic privileges")
case _:
print("Access denied - invalid user profile")Output:
Admin access granted - full system privilegesThis example demonstrates how guards can implement complex business logic. The system checks multiple conditions: account status, login attempts, and role-based permissions. Each case handles a specific scenario, making the authentication logic clear and maintainable.
13.5) Matching on Simple Sequence and Mapping Shapes
13.5.1) Matching Variable-Length Sequences
Sometimes you need to match sequences of varying lengths. Python's pattern matching supports this with the * operator, which captures zero or more elements.
# Command parser with variable arguments
command = ["copy", "file1.txt", "file2.txt", "file3.txt", "backup/"]
match command:
case ["help"]:
print("Available commands: copy, move, delete")
case ["copy", source, destination]:
print(f"Copying {source} to {destination}")
case ["copy", *sources, destination]:
print(f"Copying {len(sources)} files to {destination}")
print(f"Source files: {sources}")
case ["delete", *files]:
print(f"Deleting {len(files)} file(s): {files}")
case _:
print("Unknown command")Output:
Copying 3 files to backup/
Source files: ['file1.txt', 'file2.txt', 'file3.txt']The pattern ["copy", *sources, destination] matches a list that starts with "copy", ends with a destination, and has any number of source files in between. The *sources captures all the middle elements as a list.
Important: You can only use one * pattern per sequence pattern, and it captures elements as a list:
# Log entry parser
log_entry = ["2025-12-17", "10:30:45", "ERROR", "Database", "connection", "timeout"]
match log_entry:
case [date, time, "ERROR", *error_details]:
print(f"Error on {date} at {time}")
print(f"Error details: {' '.join(error_details)}")
case [date, time, "WARNING", *warning_details]:
print(f"Warning on {date} at {time}")
print(f"Warning details: {' '.join(warning_details)}")
case [date, time, level, *message]:
print(f"{level} on {date} at {time}: {' '.join(message)}")Output:
Error on 2025-12-17 at 10:30:45
Error details: Database connection timeout13.5.2) Combining Sequence Patterns with Guards
You can use guards with sequence patterns to add additional conditions:
# Grade list analyzer
grades = [85, 92, 78, 95, 88]
match grades:
case []:
print("No grades recorded")
case [grade] if grade >= 90:
print(f"Single excellent grade: {grade}")
case [grade] if grade < 60:
print(f"Single failing grade: {grade}")
case [*all_grades] if len(all_grades) >= 5 and sum(all_grades) / len(all_grades) >= 90:
average = sum(all_grades) / len(all_grades)
print(f"Excellent performance! Average: {average:.1f}")
case [*all_grades] if len(all_grades) >= 5:
average = sum(all_grades) / len(all_grades)
print(f"Performance review: {len(all_grades)} grades, Average: {average:.1f}")
case [*all_grades]:
print(f"Insufficient data: only {len(all_grades)} grade(s)")Output:
Performance review: 5 grades, Average: 87.6The pattern [*all_grades] captures all elements in the list, and the guard checks both the length and calculates the average to determine the appropriate message.
Pattern matching with match and case provides a powerful, expressive way to handle complex decision-making in Python. From simple value matching to sophisticated structural patterns with guards, this feature enables you to write cleaner, more maintainable code for handling multiple cases and extracting data from complex structures.
As you continue learning Python, you'll find that pattern matching complements the conditional logic you learned in Chapter 8, offering an elegant alternative when dealing with multiple distinct cases, especially when working with structured data. The key is choosing the right tool for each situation: use if-elif-else for simple conditions and boolean logic, and reach for match-case when you're checking one value against multiple possibilities or working with structured data that needs pattern-based extraction.