Python structural pattern matching

In the list of new features when version 3.10 of Python was announced, structural pattern matching was one of the most innovative features for me. While most people describe that as the Switch...case Pattern that we know in other popular programming languages like PHP or Javascript, structural pattern matching offers much more, as we will explore in this blog post. It provides a powerful mechanism for matching complex data structures and integrating seamlessly with Python's existing features.

Pattern Matching

As described in this amazing Wikipedia article, pattern matching in computer science is checking a given sequence of tokens for the presence of the constituents of some pattern. The pattern here can be a string or any other data. Pattern matching is a good alternative to conditional statements, resulting in cleaner, more readable code. It simplifies complex conditional logic and makes the code more self-explanatory.

Python's Structural Pattern Matching

In Python, match statement introduces structural pattern matching, providing a more concise and expressive way to handle conditional logic. Unlike traditional constructs like if-elif-else and switch-case(in other programming languages), which rely on sequential evaluation or equality checks, match allows a direct matching of patterns against values, enabling more robust and readable code.

Syntax and Basic Usage

The match statement in Python >= 3.10 allows for concise conditional branching by matching a given value against patterns defined in case clauses. It sequentially evaluates each pattern and executes the corresponding action for the first matching pattern encountered. Patterns are evaluated from top to bottom, with the _ wildcard pattern serving as a catch-all for unmatched values. Guards, specified using the if keyword, enable additional conditions to be applied to patterns.

def classify_value(value: int):
    match value:
        case 0:
            print("Zero")
        case n if n > 0:
            print("Positive")
        case n if n < 0:
            print("Negative")

check_sign(5)   # Output: Positive
check_sign(-3)  # Output: Negative
check_sign(0)   # Output: Zero

In this example, we define a function classify_value that takes an integer value as input and uses the match statement to match it against different literal values. Depending on the value of the input, it prints the corresponding classification.

It also works very well with strings as we can see in the following example.

def check_string(value: str):
    match value:
        case "apple":
            print("It's an apple")
        case "banana":
            print("It's a banana")
        case _:
            print("It's something else")

check_string("apple")   # Output: It's an apple
check_string("banana")  # Output: It's a banana
check_string("orange")  # Output: It's something else

In this example, the check_string function takes a string value as input and uses the match statement to match it against different patterns defined in the case clauses. If the input string matches one of the specified patterns ("apple" or "banana"), the corresponding action is executed. Otherwise, the _ wildcard pattern catches any unmatched values, and the default action is executed, indicating that it's something else.

Advanced Patterns

Pattern matching does not only work with basic types in Python, it can also be used with more complex types like lists, dict or tuples. Here are some examples.

from typing import List

def match_list(lst: List[int]):
    match lst:
        case [1, 2, _]:
            print("The first two elements are 1 and 2")
        case [x, y, z]:
            print(f"The list exacly 3 elements which are: {x}, {y}, {z}")
        case _:
            print("List does not match")

match_list([1, 2, 3])       # Output: The first two elements are 1 and 2
match_list([4, 5, 6])       # Output: The list exacly 3 elements which are: 4, 5, 6
match_list([7, 8, 9, 10])   # Output: First three elements are 7, 8, 9

In the example, the match_list function takes a string value as input and uses the match statement to match it against different patterns defined in the case clauses. If the input string matches one of the specified patterns

  • The First pattern in this case checks if the first two elements the the list are 1 and 2.

  • The Second pattern checks if the list has exactly 3 elements.

  • If none of these cases is matched, the _ wildcard pattern will be triggered and the default action is executed, indicating that it's something else.

def match_tuple(tup):
    match tup:
        case (0, _):
            print("Tuple starts with zero")
        case (_, "hello"):
            print("Tuple contains 'hello'")
        case _:
            print("Tuple does not match")

match_tuple((0, "world"))    # Output: Tuple starts with zero
match_tuple((42, "hello"))   # Output: Tuple contains 'hello'
match_tuple((10, "bye"))     # Output: Tuple does not match

This example shows how match can be used with a tuple. The first pattern checks if the first element of the tuple is 0 and the second pattern checks if the last element in the tuple is the string hello.

Pattern matching with Custom Classes

As Python developers, we write our own custom classes in most projects we work on. Another interesting aspect of match is how it works with custom classes. Basically, custom classes can define the __match_args__ attributes, which should be a tuple that defines what attributes on a class instance get used in the case expression of a match statement.

class Point:
    __match_args__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

def match_instance(obj):
    match obj:
        case Point(0, 0):
            print("Origin Point")
        case Point(x, y):
            print(f"Point at ({x}, {y})")
        case _:
            print("Not a Point instance")

point1 = Point(0, 0)
point2 = Point(1, 1)
point3 = Point(2, 2)

match_instance(point1)   # Output: Origin Point
match_instance(point2)   # Output: Point at (1, 1)
match_instance(point3)   # Output: Point at (2, 2)
match_instance("Hello")  # Output: Not a Point instance

In this example, we define a class Point with two attributes(x and y). Additionally, we also define __match_args__ with both attributes meaning both will be used in the match expression. The match_instance function then takes an object as parameters and uses it to initiate the match pattern with the following cases:

  • Point(0, 0): This pattern checks if the provided object is the origin(Point with x=0 and y = 0).

  • Point(x, y): This pattern checks if the provided object is a valid Point but different from the origin(Point with x != 0 and y != 0).

  • If none of these cases is matched, the _ wildcard pattern will be triggered and the default action is executed, indicating that it's something else.

Error Handling with Pattern Matching

Another important application of structural pattern matching in Python is error handling. it simplifies error handling and makes them more efficient, as we can see in the following use case.

class CustomErrorType1(Exception):
    def __init__(self, message):
        self.message = message

class CustomErrorType2(Exception):
    def __init__(self, message):
        self.message = message


def process_data(data):
    if data.get("status") == "success":
        result = data.get("result")
        print("Data processing successful. Result:", result)
    elif data.get("status") == "error":
        error = data.get("error")
        if isinstance(error, CustomErrorType1):
            print("Error of Type1 occurred during data processing:", error.message)
        elif isinstance(error, CustomErrorType2):
            print("Error of Type2 occurred during data processing:", error.message)
        else:
            print("Unknown error occurred during data processing")
    else:
        print("Invalid data format")

# Example usage
data1 = {"status": "success", "result": 42}
data2 = {"status": "error", "error": CustomErrorType1("Data not found")}
data3 = {"status": "error", "error": CustomErrorType2("unauthorized access")}
data4 = {"status": "invalid"}

process_data(data1)  # Output: Data processing successful. Result: 42
process_data(data2)  # Output: Error of Type1 occurred during data processing: Data not found
process_data(data3)  # Output: Error of Type2 occurred during data processing: unauthorized access
process_data(data4)  # Output: Invalid data format

This approach provides a more elegant and readable way to handle different cases, making the code more expressive and reducing the need for nested if-else blocks or cumbersome try-except constructs.

Best Practices and Tips

One of the common questions that came out about structural pattern matching in Python is the question of knowing when to use them over the traditional control flow constructs like if-elif-else. Here are some points to consider when deciding which one to use.

  • Structured Data: Pattern matching excels when working with structured data, such as dictionaries, tuples, or custom data types, where different patterns can be matched against specific attributes or values.

  • Multiple Conditions: If you have multiple conditions to check and handle based on the structure of data, pattern matching can provide a more concise and readable solution compared to nested if-elif-else blocks.

  • Error Handling: Pattern matching can be particularly useful for error handling, especially when dealing with custom exceptions or complex error scenarios. It allows elegantly handling different error or exception types and associated actions.

  • Simple Conditions: For simple conditional checks where you're only comparing values or evaluating boolean expressions, traditional control flow constructs like if-elif-else may be more straightforward and familiar.

  • Readability: In some cases, using if-elif-else statements may lead to more readable code, especially when the logic is straightforward and doesn't involve complex patterns or data structures.

  • Legacy Codebases: If you're working on a codebase that doesn't yet support Python 3.10 or where developers are not familiar with pattern matching, sticking with traditional control flow constructs may be more appropriate to maintain consistency and readability.

Conclusion

In conclusion, structural pattern matching in Python offers numerous benefits, including code readability, simplified conditional logic, and improved error handling. By providing a more concise and expressive syntax for matching patterns within data structures, Python's pattern-matching feature empowers developers to write cleaner and more maintainable code. I encourage you to explore and experiment with this powerful new feature in your own projects, leveraging its capabilities to streamline their code and unlock new possibilities. To delve deeper into structural pattern matching and its applications, I recommend consulting the official Python documentation and exploring additional resources available online.

references

0
Subscribe to my newsletter

Read articles from Alex Mboutchouang directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alex Mboutchouang
Alex Mboutchouang

Welcome to my blog! I'm Alex Mboutchouang, a passionate Python Developer with a keen interest in AI/ML and cloud technologies. I specialize in crafting small yet impactful pieces of code aimed at making a difference in the world. As an avid Open Source Lover, I actively contribute to projects over at @osscameroon. Join me as I explore the realms of programming, artificial intelligence, and beyond, sharing insights and experiences along the way.