Back to Posts
Abstract architectural structure illuminated with dramatic lighting, demonstrating principles of structural pattern matching in a Python programming context.

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.

Improve your code with my 3-part code diagnosis framework

Watch my free 30 minutes code diagnosis workshop on how to quickly detect problems in your code and review your code more effectively.

When you sign up, you'll get an email from me regularly with additional free content. You can unsubscribe at any time.

Recent posts