Putting the Structural in Structural Pattern Matching

Explore the power of structural pattern matching in Python. Learn to replace cumbersome if-elif-else chains for more clarity and efficiency. Stay tuned for our next article on matching tuples, lists, and dictionaries. Subscribe for more insights into advanced Python programming!

Putting the Structural in Structural Pattern Matching
A Python on top of a three-dimensional puzzle

Overview

After the first look at structural pattern matching, we'll examine why the structural is part of the name.

In languages like Java or C, switch statements help us choose a code branch based on a variable's value. They're great for making decisions without writing many 'if-else' statements. But they have a limit. They can't handle complex data like lists that require us to write extra code.

Another challenge is the 'fallthrough' behavior in C and Java. Without a break statement, a switch in C will continue executing the subsequent cases, which can be confusing. This is a poor choice for mapping multiple matches to the same action.

Java improved upon C by introducing the ability to use Strings in switch statements, a feature that added more flexibility and expressiveness. Despite these improvements, Java and C switch statements cannot handle complex data structures or perform pattern-based matching elegantly.

In Python, we didn't have switch statements. Instead, we used many 'elif' (else if) statements. This worked but made our code ugly, long, and hard to read. It was especially tricky with complicated data.


Painful Patterns: Coding Without Pattern Matching

Man with axe yells in a tunnel of twisted thorns.
A screaming developer fights against thorn-full code.
Java C Python < 3.10

public class SwitchExample {
    enum Weekday {
        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
    }

    static class DayInfo {
        Weekday day;
        int date;

        DayInfo(Weekday day, int date) {
            this.day = day;
            this.date = date;
        }
    }

    public static void main(String[] args) {
        DayInfo today = new DayInfo(Weekday.WEDNESDAY, 15);

        switch (today.day) {
            case MONDAY:
            case TUESDAY:
            case THURSDAY:
            case FRIDAY:
                System.out.println("It's a regular workday.");
                break;
            case WEDNESDAY:
                System.out.println("It's Wednesday, the middle of the work week.");
                // Check for a specific date within the case
                if (today.date == 15 || today.date == 30) {
                    System.out.println("It's Salary Day!");
                }
                break;
            case SATURDAY:
            case SUNDAY:
                System.out.println("It's the weekend!");
                break;
        }
    }
}
        

from enum import Enum

class Weekday(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3
    THURSDAY = 4
    FRIDAY = 5
    SATURDAY = 6
    SUNDAY = 7

class DayInfo:
    def __init__(self, day, date):
        self.day = day
        self.date = date

def process_day(day_info):
    if day_info.day in [Weekday.MONDAY, Weekday.TUESDAY, Weekday.THURSDAY, Weekday.FRIDAY]:
        return "It's a regular workday."
    elif day_info.day == Weekday.WEDNESDAY:
        message = "It's Wednesday, the middle of the work week."
        if day_info.date == 15 or day_info.date == 30:
            message += " It's Salary Day!"
        return message
    elif day_info.day in [Weekday.SATURDAY, Weekday.SUNDAY]:
        return "It's the weekend!"
    else:
        return "Invalid day."

today = DayInfo(Weekday.WEDNESDAY, 15)
print(process_day(today))
        

In the previous examples in C, Java, and Python, we demonstrate an approach to handling conditional logic in each language, and we see the limitation of each language in its approach to the task.

The C example, using enums and switch statements, is structured but rigid. This rigidity becomes apparent when considering complex scenarios like additional special dates. Extending the C code to handle more nuanced conditions would likely lead to nested if-else blocks within each case, resulting in cumbersome and error-prone code.

Java offers more flexibility, mainly because it supports Strings in switch statements. However, this advantage still needs to be improved in managing multiple data attributes simultaneously. For instance, adding logic for special events like "Salary Day" or "Spooky Day" on Friday the 13th requires extra conditional checks outside the switch logic. This makes the Java code challenging to maintain and extend as the number of cases increases.

Python, before the advent of pattern matching, relies on if-elif-else chains. While this approach is more flexible than C's switch statement and avoids the fall-through issue, it becomes unwieldy with the introduction of complex conditions. Extending the Python code to accommodate different types of special events or varying conditions for each day can quickly lead to a bloated and convoluted series of conditionals. This makes the code hard to read and increases the likelihood of bugs.

The core challenge lies in their inability to handle multi-faceted conditions efficiently in all these examples. As new scenarios are introduced, the complexity of managing them increases exponentially. This is particularly evident when dealing with multiple attributes of a day, such as its name, date, and special significance.

The traditional C, Java, and Python approaches need a unified, streamlined way to destructure and process complex data types within their respective conditional constructs. This limitation underscores the need for an advanced solution like Python's structural pattern matching, which offers a more elegant and error-resistant way to handle multiple-path logic.


Easing the Pain: Streamlining Code with Pattern Matching

Python 3.12

from enum import Enum

class Weekday(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3
    THURSDAY = 4
    FRIDAY = 5
    SATURDAY = 6
    SUNDAY = 7

class DayInfo:
    __match_args__ = ("day", "date")

    def __init__(self, day, date):
        self.day = day
        self.date = date

def process_day(day_info):
    match day_info:
        case DayInfo(day=Weekday.WEDNESDAY, date=15) | DayInfo(day=Weekday.WEDNESDAY, date=30):
            return "It's Wednesday, Salary Day!"
        case DayInfo(day=Weekday.FRIDAY, date=13):
            return "It's Spooky Day (Friday the 13th)!"
        case DayInfo(day=_, date=13):
            return "It's the 13th, but the day of the week doesn't matter."  
        case DayInfo(day=Weekday.SATURDAY) | DayInfo(day=Weekday.SUNDAY):
            return "It's the weekend!"
        case DayInfo():
            return "It's a regular day."
        case _:
            return "Invalid day."

today = DayInfo(Weekday.WEDNESDAY, 15)
print(process_day(today))
        

In our enhanced Python example, the key to unlocking the power of structural pattern matching lies in how we structure our DayInfo class. By defining the __match_args__ attribute in line 13th, we've instructed Python how to destructure objects of this class in a match statement. This attribute tells Python, "These are the attributes of my class that can be used in pattern matching".

When we use this class in a match statement, we start with the class name, DayInfo, followed by specifying the attributes, and for that, we can either:

  1. Use Specific Values: If we use a value, python will look into the matched instance and check if the value equals the one we specified. For example, if we want to deal only with Wednesdays, we would write day=Weekday.WEDNESDAY.
  2. Using Variable Names: Alternatively, we can write a valid identifier. Python will match any value and then let the value into a variable with the name we pass in. For example, day=any_day will assign the day's value to a variable called any_day.

Here’s a short example illustrating both:

Python 3.12

match day_info:
    case DayInfo(day=Weekday.WEDNESDAY, date=15):
        return "It's a specific day - Wednesday the 15th."
    case DayInfo(day=any_day, date=13):
        return f"It's the 13th, but on a {any_day}."
        

An important aspect of structural pattern matching in Python is how it handles the matching process. Python's pattern matching operates on a 'first match' principle, meaning that once a match is found, the corresponding block of code is executed, and the match statement concludes its execution. This behavior ensures that only one pattern can match during each run of the match statement.

Furthermore, Python's pattern matching does not have fallthrough behavior. Once a case matches, the execution of the match statement is complete, and it does not continue to evaluate the rest of the cases. This design decision simplifies the control flow and prevents the unintentional execution of multiple case blocks, a common source of bugs in languages that feature fallthrough in switch statements.

If we want to use multiple patterns, we'll use OR patterns, like the one in line 21st. In this line, the OR pattern combines conditions within a single case. This case will execute if day_info matches any specified patterns (the 15th or 30th of a Wednesday). Importantly, if there are other cases below this one, they won't be evaluated once a match is found here.

In line 25th, we use an underscore (case DayInfo(day=_, date=13)) that will match any value but won't capture it in any variable. This is practical when we do not care about the value and want to avoid polluting the namespace with unused variables.

Line 29th is a little different. It simply checks the variable type without looking into the attributes, and It is helpful when dealing with polymorphic inputs.

Wizard on a serpent in a digital forest with Python icons and code rain
Ride on the back of Python's power

Conclusion

We've journeyed through Python's transformative world of structural pattern matching, exploring how it streamlines complex conditional logic and enhances code readability.

By replacing convoluted if-elif-else chains with clear and concise patterns, we've demonstrated how Python programmers can now tackle intricate data scenarios with newfound ease and confidence. This feature simplifies code and aligns Python with more advanced programming paradigms, marking a significant stride in the language's evolution.

Stay tuned for the next installment in this series, where we'll dive deeper into structural pattern matching by focusing on tuples, lists, and dictionaries.

If you found this article insightful and are eager for more, consider subscribing to our content. We greatly appreciate paid subscriptions, as they support our work directly, but we warmly welcome all subscribers.


Addendum: A Special Note for Our Readers

I decided to delay the introduction of subscriptions, you can read the full story here.

In the meantime, I decided to accept donations.

If you can afford it, please consider donating:

Every donation helps me offset the running costs of the site and an unexpected tax bill. Any amount is greatly appreciated.

Also, if you are looking to buy some Swag, please visit I invite you to visit the TuringTacoTales Store on Redbubble.

Take a look, maybe you can find something you like: