Introduction to Structural Pattern Matching in Python
By Alyce Osbourne
Ever found yourself tangled in the web of code while trying to assess both structure and data? If you’ve spent any time with isinstance
and hasattr
, you know the pain. These methods work, but they’re often clunky and cumbersome. What if there was a more elegant solution that not only simplifies your code but also makes it more readable?
The answer is Structural Pattern Matching (SPM). In this post, we’ll dive into how SPM can revolutionize your approach to pattern analysis and parsing, turning chaos into clarity.
What is Structural Pattern Matching?
Structural pattern matching is a feature that allows you to check a value against a pattern, extracting parts of the value if the pattern matches. This can make complex conditionals simpler and more readable.
Basic syntax
Here’s a quick look at the basic syntax:
def http_status(status: int) -> str:
match status:
case 200:
return "OK"
case 404:
return "Not Found"
case 500:
return "Internal Server Error"
case _:
return "Unknown Error"
In the example above, match
is a keyword introduced in Python 3.10, which, combined with case
, allows for clean and readable pattern matching. Expressions are evaluated from top to bottom, meaning the first true statement will be the one that is used. Therefore, it’s recommended that patterns of higher specificity come higher to avoid being matched by more generalized statements.
Using patterns
Patterns can range from simple constants to more complex structures. Let’s explore a few examples:
Constant patterns
Constant patterns match literal values.
value = 42
match value:
case 0:
print("Zero")
case 3.14:
print("Pi")
case 42:
print("The answer to life, the universe, and everything")
case "Some Name":
print("Hello")
case _:
print("Something Else")
Variable patterns
Variable patterns capture the matched value.
value = 42
match value:
case int() as i:
print(f"Got an int variable: {i}")
case float() as f:
print(f"Got a float variable: {f}")
case str() as s:
print(f"Got a str variable: {s}")
case unknown:
print(f"Got an unknown object: {unknown}")
Note
When matching instances of any type, you need to match against an object pattern. This means using the type_name() syntax, similar to instantiating an object. This is why I use int() instead of int.
Sequence patterns
Sequence patterns match against lists, tuples, and other sequences.
data = [1, 2, 3]
match data:
case [1, 2, 3]:
print("Matched [1, 2, 3]")
case [1, 2, _]:
print("Matched [1, 2, _]")
case [int(), int(), int()]:
print("Matched [int(), int(), int()]")
case _:
print("No match")
Mapping patterns
Mapping patterns match against dictionaries.
config = {"host": "localhost", "port": 8080, "token": "<TOKEN>"}
match config:
case {"host": host, "port": port}:
print(f"Host: {host}, Port: {port}")
case _:
print("No match")
It will only match against the keys defined in the match statement, ignoring other keys.
Conditional patterns
Patterns can match objects, with optional extra evaluation.
status = 404
match status:
case int() if 0 <= status < 100:
print("Informational")
case int() if 100 <= status < 200:
print("Success")
case int() if 200 <= status < 300:
print("Redirection")
case int() if 300 <= status < 400:
print("Client Error")
case int() if 400 <= status < 500:
print("Server Error")
case _:
print("Unknown error code")
Combining patterns
Patterns can be combined using |
(OR).
status = 200
match status:
case 200 | 201:
print("Success")
case 400 | 404:
print("Client Error")
case _:
print("Other Error")
Adding matching to custom objects
To enable pattern matching on custom objects, you need to implement certain dunder (double underscore) methods in your classes. These methods help the match
statement understand how to destructure your objects.
__match_args__
The __match_args__
attribute is a tuple that specifies the attribute names for pattern matching. By default, it’s an empty tuple, meaning no attributes can be extracted.
class Point:
__match_args__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
Matching the object
point = Point(10, 20)
match point:
case Point(10, 20):
print("Home")
case Point(x, y):
print(f"Point with {x=}, {y=}")
case _:
print("Unknown Position")
In this example, we create an instance of Point
and then use structural pattern matching to extract the x
and y
attributes. If the object matches the Point
pattern, it prints the coordinates; otherwise, it prints “Unknown Position.”
Final thoughts
Python’s Structural Pattern Matching is a highly valuable tool that I regularly utilize in my projects, whether I am developing parsers, validating network data, or conducting tests. It seamlessly integrates with Python’s syntax, making it intuitive and straightforward to comprehend. It can greatly streamline intricate if/else chains while also offering a neat and efficient method for capturing unforeseen values.