Python Match
Python's match Statement: Structural Pattern Matching (Python 3.10+)
The match statement allows you to compare a subject (an expression) against several patterns and
execute a block of code when a pattern matches. It's a more declarative and often more readable
alternative to long if-elif-else chains, particularly when your conditions involve inspecting the
structure or shape of data.
It's inspired by pattern matching features found in functional programming languages.
1. Basic match Statement:
The simplest form involves matching a single value against literal patterns.
Syntax :
match subject:
case pattern1:
# Code to execute if subject matches pattern1
case pattern2:
# Code to execute if subject matches pattern2
case _: # The "wildcard" pattern (like 'else')
# Code to execute if no other pattern matches
Key Points :
- subject: The expression whose value you want to match.
- case: Introduces a pattern.
- pattern: What you are trying to match the subject against.
- _ (Wildcard Pattern): Catches anything that hasn't been matched by previous case statements.
It acts like an else block and should typically be the last case.
Example 1.1: Matching a string (commands) :
command = "start"
match command:
case "start":
print("System is starting...")
case "stop":
print("System is shutting down.")
case "restart":
print("System is restarting now.")
case _: # Default case if no match found
print(f"Unknown command: '{command}'")
# Another example:
command = "status"
match command:
case "start":
print("System is starting...")
case "stop":
print("System is shutting down.")
case "restart":
print("System is restarting now.")
case _:
print(f"Unknown command: '{command}'")
Explanation :
The command variable is the subject. Each case checks if command exactly matches the string literal
("start", "stop", "restart"). If command is "status", it doesn't match any of the explicit cases, so
the wildcard _ case is executed.
2. Matching Multiple Literals with | (OR operator) :
You can match against multiple literal values using the | (OR) operator within a single case.
Example 2.1: Matching multiple HTTP status codes :
status_code = 403
match status_code:
case 200:
print("OK: Request successful.")
case 400 | 401 | 403 | 404: # Matches any of these client errors
print("Client Error: Please check your request or authentication.")
case 500 | 502 | 503: # Matches any of these server errors
print("Server Error: Something went wrong on the server side.")
case _:
print(f"Unhandled status code: {status_code}")
Explanation :
If status_code is 403, it matches 400 | 401 | 403 | 404 and the corresponding message is printed.
3. Capturing Patterns (Variable Patterns) :
You can capture parts of the subject into variables for later use within the case block.
This is done by using a variable name as a pattern.
Example 3.1: Capturing a simple value :
action = "download"
match action:
case "upload":
print("Initiating file upload.")
case "download" as file_action: # Captures "download" into file_action
print(f"Initiating file {file_action}.")
case cmd: # Captures any other value into 'cmd'
print(f"Executing generic command: {cmd}")
# Another example:
action = "process"
match action:
case "upload":
print("Initiating file upload.")
case "download" as file_action:
print(f"Initiating file {file_action}.")
case cmd: # 'process' is captured by 'cmd'
print(f"Executing generic command: {cmd}")
Explanation :
When action is "download", it matches case "download" as file_action:, and the string
"download" is assigned to the file_action variable. If action is "process", it doesn't match
the specific literals, so it falls to case cmd:, and "process" is assigned to cmd.
4. Matching Sequence Patterns (Lists and Tuples) :
You can match against the structure and elements of sequences like lists and tuples.
Key Points:
- Exact length matching: case [a, b, c] will only match a list of exactly three elements.
- Star pattern (*):Matches zero or more elements. It captures them into a list. * can appear only once in a sequence pattern.
- Nested patterns: You can nest patterns within sequences.
Example 4.1: Matching coordinates :
point1 = (1, 2)
point2 = [5, 10, 15]
point3 = (0, 0)
point4 = (3, -1)
for point in [point1, point2, point3, point4]:
match point:
case (0, 0):
print(f"{point}: Origin.")
case (x, y): # Matches any tuple of two elements, captures them into x, y
print(f"{point}: Point at ({x}, {y}).")
case [x, y, z]: # Matches a list of three elements
print(f"{point}: 3D point at [{x}, {y}, {z}].")
case _:
print(f"{point}: Unrecognized point format.")
Explanation :
- (0, 0) matches the specific literal tuple.
- (x, y) matches any 2-element tuple, capturing its elements.
- [x, y, z] matches any 3-element list.
- _ matches anything else, acting as a catch-all.
Example 4.2: Using the star pattern * (variable-length sequences)
data_entry1 = ["log", "info", "User logged in."]
data_entry2 = ["error", "Permission denied.", "main.py", 102]
data_entry3 = ["event", "App startup"]
for entry in [data_entry1, data_entry2, data_entry3]:
match entry:
case ["log", level, message]: # Matches a 3-element list with specific first element
print(f"LOG ({level.upper()}): {message}")
case ["error", message, *details]: # Matches "error", captures message, and remaining elements into 'details' list
print(f"ERROR: {message} (Details: {details})")
case ["event", *args]: # Matches "event" and captures remaining elements into 'args'
print(f"EVENT: {args}")
case _:
print(f"Unknown data entry: {entry}")
Explanation :
- ["log", level, message] requires exactly 3 elements, starting with "log". level and message capture the second and third elements.
- ["error", message, *details] requires at least 2 elements, starting with "error".
message captures the second, and details captures all subsequent elements as a list (can be empty).
5. Matching Mapping Patterns (Dictionaries):
You can match against the structure and keys/values of dictionaries.
Key Points:
- You can specify required keys and capture their values.
- **rest (double-star) captures remaining key-value pairs into a new dictionary.
Example 5.1: Matching user profiles :
user1 = {"name": "Alice", "age": 30, "city": "New York"}
user2 = {"name": "Bob", "role": "admin"}
user3 = {"username": "charlie_dev"}
user4 = {"name": "David", "age": 17, "status": "active"}
for user in [user1, user2, user3, user4]:
match user:
case {"name": name, "age": age} if age >= 18: # Matches if 'name' and 'age' keys exist, and age is >= 18
print(f"Adult User: {name}, Age: {age}")
case {"name": name, "age": age}: # Matches if 'name' and 'age' keys exist (any age)
print(f"Minor User: {name}, Age: {age}")
case {"name": name, "role": role}: # Matches if 'name' and 'role' keys exist
print(f"User with Role: {name}, Role: {role}")
case {"name": name, **other_info}: # Matches if 'name' exists, captures rest into other_info dict
print(f"User (Name only): {name}, Other info: {other_info}")
case _:
print(f"Unknown user format: {user}")
Explanation :
- {"name": name, "age": age} matches if both name and age keys are present. Their values are
captured into name and age variables.
6. Guards (if clauses in case):
You can add an if clause (a "guard") to a case pattern. The case only matches if both the pattern
matches and the if condition is true.
Syntax:
case pattern if condition:
# Code to execute if pattern matches AND condition is True
Key Points:
- The condition can use any variables captured by the pattern.
- Guards allow for more fine-grained control beyond just structural matching.
Example 6.1: Filtering numbers
num = 7
match num:
case n if n % 2 == 0:
print(f"{n} is an even number.")
case n if n % 2 != 0:
print(f"{n} is an odd number.")
case _:
print("Not a number?")
Example 6.2: Combining patterns and guards (from user profile example)
user = {"name": "Charlie", "age": 15, "city": "London"}
match user:
case {"name": name, "age": age} if age >= 18:
print(f"Adult user {name} ({age}).")
case {"name": name, "age": age} if age < 18:
print(f"Minor user {name} ({age}).")
case {"name": name, **details}:
print(f"User {name} with other details: {details}")
case _:
print("Invalid user data.")
Explanation :
The if age >= 18 acts as a guard. The case will only activate if user has name and age keys and
the value of age is 18 or greater.
7. Class Patterns (Matching Objects):
This is one of the most powerful features. You can match against instances of classes and extract attributes.
Syntax
case ClassName(attribute1=var1, attribute2=var2, ...):
# Code to execute if subject is an instance of ClassName
# and attributes match/are captured
Key Points:
- The match statement calls __match_args__ (if defined in the class) or looks for attributes by name.
- You can mix literal values for attributes or capture them into variables.
- You can use nested class patterns.
Example 7.1: Matching geometric shapes
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
# Optional: Define __match_args__ for positional matching (more advanced)
def __match_args__(self):
return ("x", "y")
class Circle:
def __init__(self, center, radius):
self.center = center # center is a Point object
self.radius = radius
def __match_args__(self):
return ("center", "radius") # Allows matching like Circle(Point(x,y), r)
class Rectangle:
def __init__(self, top_left, bottom_right):
self.top_left = top_left
self.bottom_right = bottom_right
# Create some shape instances
p1 = Point(10, 20)
p_origin = Point(0, 0)
c1 = Circle(Point(5, 5), 10)
r1 = Rectangle(Point(0, 10), Point(20, 0))
shapes = [p1, p_origin, c1, r1, "not a shape"]
for shape in shapes:
match shape:
case Point(0, 0): # Matches Point object where x is 0 and y is 0
print(f"Shape: Origin Point at ({shape.x}, {shape.y})")
case Point(x=x_coord, y=y_coord): # Matches any Point object, captures x and y
print(f"Shape: Generic Point at ({x_coord}, {y_coord})")
case Circle(center=Point(cx, cy), radius=r): # Nested pattern: Circle with a Point center
print(f"Shape: Circle centered at ({cx}, {cy}) with radius {r}")
case Rectangle(top_left=Point(tx, ty), bottom_right=Point(bx, by)):
print(f"Shape: Rectangle from ({tx}, {ty}) to ({bx}, {by})")
case _:
print(f"Shape: Unknown/Unmatched ({type(shape)})")
Explanation :
- Point(0, 0): Matches a Point object whose x and y attributes are both 0.
- Point(x=x_coord, y=y_coord): Matches any Point object and captures its x and y attributes into new variables x_coord and y_coord.
- Circle(center=Point(cx, cy), radius=r): This is a nested class pattern. It matches a Circle object.
Inside the Circle pattern, it expects a center attribute which itself must be a Point object.
It then extracts cx and cy from that nested Point and r from the Circle's radius attribute.
8. as keyword for Alias (Subpatterns) :
You can use the as keyword to give an alias to a subpattern, allowing you to refer to the matched part of the subject directly.
Example 8.1: Capturing the entire matched object or sub-structure
data_item1 = ["command", "start"]
data_item2 = ("log", "info", "User logged in.")
data_item3 = {"type": "user", "id": 123}
for item in [data_item1, data_item2, data_item3]:
match item:
case ["command", action] as full_command: # Captures 'action' and the entire list as 'full_command'
print(f"Detected command: {action} (Full: {full_command})")
case ("log", level, _) as log_entry if level == "error": # Captures level, ignores third, and captures full tuple
print(f"Critical Log Entry: {log_entry}")
case {"type": "user", "id": user_id} as user_obj: # Captures user_id and the entire dict as user_obj
print(f"Matched User object: ID {user_id}, Object: {user_obj}")
case _:
print(f"Unmatched item: {item}")
Explanation :
In case ["command", action] as full_command:, action gets the second element, and full_command gets
the entire list ["command", "start"]. This is useful when you need to inspect individual parts but also
operate on the whole matched structure.
9. Underscore _ as a Wildcard or Ignored Value :
The underscore _ has two main uses:
- Wildcard Pattern: As seen in case _:, it matches anything and is used as the default.
- Ignored Value: Within a pattern, _ means "match this position/attribute, but I don't care about its value, so don't bind it to a variable."
Example 9.1: Ignoring values in sequence patterns
point_3d = (10, 20, 30)
point_2d = (5, 8)
invalid_point = (1, 2, 3, 4)
for p in [point_3d, point_2d, invalid_point]:
match p:
case (x, y, _): # Matches a 3-element tuple, captures first two, ignores third
print(f"3D point (first two coords): ({x}, {y})")
case (x, _): # Matches a 2-element tuple, captures first, ignores second
print(f"2D point (first coord): ({x})")
case _:
print(f"Not a recognized point: {p}")
Explanation :
case (x, y, _) matches (10, 20, 30), captures 10 to x, 20 to y, and discards 30.
Advantages of match-case over if-elif-else :
- Readability: Especially for complex, nested data structures, match makes the code much cleaner and
easier to understand by explicitly showing the expected structure.
- Exhaustiveness Checks (potential future tooling): While not a built-in feature of Python's
runtime, pattern matching in other languages often allows static analysis tools to warn you if you
haven't covered all possible cases, which could be a future benefit for Python.
- Conciseness: It can significantly reduce boilerplate code compared to multiple if and and conditions.
- No Fall-through: Unlike some other languages (like C's switch),
Python's match does not fall through. Only the first matching case is executed.
- Direct Deconstruction: It directly deconstructs values (like extracting elements from a list or
attributes from an object) into variables, which reduces manual assignments.
When to use match-case?
- Handling different types of data/objects: When your logic depends on the type or class of
an object.
- Processing structured data: Ideal for parsing messages, commands, or data from APIs where
the shape of the data varies.
- Finite state machines: When your program's behavior changes based on its current state and incoming events.
- Replacing long if-elif-else chains that primarily check the type or structure of a variable.
When if-elif-else might still be better?
- Simple boolean conditions: When you're just checking if a single variable is greater than,
less than, or equal to a value, if-elif-else is often more direct.
- Complex arithmetic or logical operations: If your conditions involve extensive calculations
or complex and/or combinations that don't involve deconstructing data.
- Python versions older than 3.10: Obviously, match is not available.
Here are 5 simple and easy-to-understand examples of the match statement in Python:
Note: These examples require Python 3.10 or higher.
Example 1: Match a String
day = "Sunday"
match day:
case "Monday":
print("Start of the week")
case "Sunday":
print("Relax! It's weekend.")
case _:
print("Just a regular day")
#Output- Relax! It's weekend.
Example 2: Match a Number
number = 5
match number:
case 1:
print("One")
case 5:
print("Five")
case _:
print("Unknown number")
#Output- Five
Example 3: Match with Multiple Values
fruit = "apple"
match fruit:
case "apple" | "banana" | "mango":
print("It's a fruit we love!")
case _:
print("It's some other fruit.")
#Output- It's a fruit we love!
Example 4: Match a Tuple
point = (0, 5)
match point:
case (0, 0):
print("Origin")
case (0, y):
print(f"Y-axis at y = {y}")
case (x, 0):
print(f"X-axis at x = {x}")
case (x, y):
print(f"Point is at ({x}, {y})")
#Output- Y-axis at y = 5
Example 5: Match with Condition (Guard)
age = 17
match age:
case x if x < 18:
print("You are a minor.")
case x if x >= 18:
print("You are an adult.")
#Output- You are a minor.
Conclusion :
The match statement is a powerful addition to Python's control flow mechanisms. It encourages a
more declarative style of programming when dealing with varied data structures and provides a cleaner,
more readable way to express complex conditional logic. While it takes some getting used to, mastering
match will undoubtedly make your Python code more elegant and maintainable for many real-world
scenarios.