15.5 Matching Literals, Sequences, Mappings, and Class Patterns
Matching Literal Patterns
Literal patterns match against specific, concrete values like integers, floats, strings, and the None, True, and False constants. This is the simplest form of pattern matching, acting as a more powerful and readable alternative to a long chain of elif statements comparing a value for equality.
The pattern case "admin": is equivalent to if subject == "admin":. However, the key difference lies in the structure and exhaustiveness. A match-case statement encourages the programmer to consider all possible known cases explicitly, whereas an if-elif-else chain can easily miss a case or become convoluted.
def handle_http_status(code):
match code:
case 200:
return "Success"
case 404:
return "Not Found"
case 500:
return "Internal Server Error"
case _: # The wildcard pattern, acting as the default 'else'
return "Unknown status code"
print(handle_http_status(404)) # Output: Not Found
print(handle_http_status(301)) # Output: Unknown status code
Why it works this way: The match statement takes the subject (code) and checks it for equality against each pattern in sequence. The first pattern that matches wins, and its associated block is executed. It uses the same underlying mechanism as the == operator, making it intuitive for built-in immutable types.
Matching Sequence Patterns
Sequence patterns match against iterable types like lists, tuples, and ranges. They allow you to deconstruct a sequence based on its structure, checking its length and the values of its individual elements. This is far more expressive than manually checking len(sequence) and then accessing each index.
The pattern can include literal values, capture variables (to bind parts of the sequence to names), and the wildcard _. Using *<variable> allows you to capture a variable-length “rest” of the sequence, similar to starred expressions in assignments.
def process_command(command):
match command:
case ["exit"]:
print("Shutting down.")
case ["load", filename]:
print(f"Loading file: {filename}.")
case ["save", filename, *other_args]:
print(f"Saving to {filename} with options: {other_args}.")
case _:
print("Unrecognized command.")
process_command(["load", "data.txt"]) # Output: Loading file: data.txt.
process_command(["save", "a.txt", "--force", "--backup"]) # Output: Saving to a.txt with options: ['--force', '--backup'].
process_command(["delete"]) # Output: Unrecognized command.
Common Pitfall: Sequence patterns match any sequence type, not just lists. The pattern case [x, y]: would match both (1, 2) and [1, 2]. If you need to match a specific type, you must combine it with a Class pattern (e.g., case tuple((x, y)):).
Matching Mapping Patterns
Mapping patterns match against dictionary-like objects. They check for the presence of specific keys and can simultaneously extract the corresponding values. Crucially, the pattern can specify a subset of keys; extra keys in the subject do not prevent a match, which is the default behavior for dictionaries.
The pattern syntax mirrors dictionary literals. case {"name": n, "age": a}: will match any dictionary that has at least the keys "name" and "age". The values for those keys are bound to the variables n and a.
def describe_user(user_info):
match user_info:
case {"type": "admin", "name": name, "permissions": perm}:
print(f"Admin {name} has permissions: {perm}.")
case {"type": "user", "name": name}:
print(f"User {name}.")
case {"name": name}:
print(f"Unknown type for user {name}.")
case {}:
print("Empty user record.")
case _:
print("Invalid data structure.")
describe_user({"type": "admin", "name": "Alice", "permissions": "rwx"})
# Output: Admin Alice has permissions: rwx.
describe_user({"type": "user", "name": "Bob", "email": "bob@example.com"}) # 'email' is ignored
# Output: User Bob.
describe_user({"name": "Charlie"})
# Output: Unknown type for user Charlie.
Best Practice: To enforce that no extra keys are present, you can use a guard. For example: case {"type": "user", "name": name} if len(user_info) == 2: would only match a dictionary with exactly those two keys.
Matching Class Patterns
Class patterns are the most powerful feature of structural pattern matching. They allow you to deconstruct objects of custom classes, checking the type of the subject and the values of its attributes. The pattern syntax is designed to resemble the class’s constructor, making it intuitive.
The pattern case Point(x=0, y=0) checks that the subject is an instance of the Point class (or a subclass) and that its x and y attributes are both equal to 0. It does not call the class’s constructor; it reads the object’s attributes directly.
class Point:
__match_args__ = ("x", "y") # Defines positional order for matching
def __init__(self, x, y):
self.x = x
self.y = y
def locate_point(point):
match point:
case Point(x=0, y=0):
print("Origin")
case Point(x=0, y=y): # Captures the y attribute
print(f"Y={y}")
case Point(x=x, y=0): # Captures the x attribute
print(f"X={x}")
case Point(x, y): # Uses __match_args__ for positional matching
print(f"X={x}, Y={y}")
case _:
print("Not a point")
locate_point(Point(0, 0)) # Output: Origin
locate_point(Point(0, 7)) # Output: Y=7
locate_point(Point(5, 0)) # Output: X=5
locate_point(Point(3, 4)) # Output: X=3, Y=4
Why __match_args__ is Critical: For positional matching in class patterns (case Point(x, y):), Python needs to know the order of attributes. The __match_args__ class variable provides this order. If it’s not defined, you can only use keyword-style patterns (case Point(x=1, y=2):). Defining __match_args__ is a best practice for classes intended to be used with pattern matching.