Beka Modebadze | All You Need To Know About Python 3.10's Pattern Matching

Finally, Gods of Python decided to add a highly requested feature pattern matching. As a part of the PEP634, a structural pattern matching feature has been added to Python 3.10 and currently is in beta mode for testing. Why is this a good news and how does structural pattern matching will make Python developer's life easier? In this post I'll:

    - Introduce what is a structural pattern matching and it's syntax.
    - What's the difference between switch and match statements?
    - Where does match statement fits in Python and it's advantages over if/elif/else statement

Structural Pattern Matching

Soon (Official release is planned in October) we'll see a Python's structural pattern matching with a form of match/case statement. Pattern matching will be available for primitive data types matching, extracting data from more complex data types, and then applying specific instructions based on retrieved values.

Example syntax for Python's pattern matching:

When the pattern is evaluated case compares value to the pattern_1 if it's true action_1 will be executed and the program will break. If evaluated result is false then case #2 will copared to pattern_2 and if matched action_2 will be executed, etc. up until program reaches _ which stands for default execution, if no other match is found.

If we left out _ case from our code and none of the other cases match, default will be a no-op that is similar to pass i.e. no action will be conducted. We can also use a | ("or") operator if we want to evaluate two or more cases for a single action.

This is a simple review of the syntax of this anticipated feature. Before diving into examples and use cases let's explore what's the difference between switch and match as often these words are used interchangeably but mean two different attributes.

Switch vs Match

Many popular programming languages like Java, C, C++ have a switch statement. It is used extensively due to its readability and fast execution speed compared to if/else statements when you have to evaluate multiple if-else cases. On a lower level switch case is executed faster compared to multiple if-else statements.

This is because the switch evaluates the data type of the subject and creates a look-up/hash table of the expected cases. Then comparison to identify which case to execute can be done in a near-constant time. The difference in runtime is minuscule for 99% of the time but good readability and clean design give preference to the switch cases on many occasions.

However, limitations of the switch case is that, for example in C it can't go beyond primitive data types like int, char, and enum. For example, let's look at this simple C code:

    #include <stdio.h>

    int main()
    {
        float val = 2.1;
        switch (val) {
            case 1.1:   printf("Value = %f\n", val);
            case 2.1:   printf("Value = %f\n", val);
            default:    printf("No match found");
        }
        return 0;
    }
    

If we try to pass a String or a float we'll get a compiler error (though Strings are allowed in Java):

error: statement requires expression of integer type ('float' invalid)

   switch (val) {
   ^       ~~~

1 error generated.

So here is the difference with match statement. It can be used as a statement (to just evaluate primitive data types) and as an expression. What's the difference between statement and expression? The rule of thumb is that statements are instructions. They perform actions. Expressions hold data; they resolve to some value. So we can do more complex pattern evaluations using match statement/expression, which we'll demonstrate below.


Match Case Examples

Python pattern matching can be used in diverse ways. It can be used as a match statement for the literals and a match expression for more complex situations. We can capture patterns in the following style:

match string:
    case "":
        print('The boy has no name')
    case "Edward":
        print(f'His name is {string}')

We can use an underscore _ as a wildcard. This is a special kind of pattern that always matches but never binds:


match point:
        case (x, _):
        print(f'x is located at {x}')

Mapping pattern is also a thing that allows matching a key, value pair. It's done by unpacking the iterable where both key and value are evaluated as patterns. The syntax is similar to dictionary display and can even use **rest to extract remaining items from the data:

match score:
    case {'five': 5}:
        return 5.1
    case {'four': 4}:
        return 4.1
    case {**rest}:
        return 0.1

The variety of patterns we can match allows us to get creative and I'm sure the Python community will give us a great use case. Now, let's look at how it compares to the if/else statement and how we can match classes and a variety of other patterns within a single run.

Match-Case vs If-Elif-Else

Let's say we've two classes Color that has three mandatory fields for red, green, and blue and StabilizedColor which has all above mentioned fields plus an alpha field. We want to have a function that takes various types of numerical arguments (and not only) and returns StabilizedColor object. This's how we'd implement it using match expression.

    from dataclasses import dataclass

    @dataclass
    class Color:
        red: int
        green: int
        blue: int
    
    @dataclass
    class StabilizedColor:
        red: int
        green: int
        blue: int
        alpha: int
    
    def stabilize_rgb_color(color) -> StabilizedColor:
        match color:
            case (r, g, b, a):
                return StabilizedColor(r, g, b, a)
            case (r, g, b):
                return StabilizedColor(r, g, b, 0)
            case Color(r, g, b):
                return StabilizedColor(r, g, b, 0)
            case StabilizedColor(_, _, _, _):
                return color
            case _:
                raise TypeError("Only supports RGB color")
    
    # match based on a tuple
    print(stabilize_rgb_color((123, 22, 22)))
    print(stabilize_rgb_color((123, 22, 31, 1)))
    
    # match based on a list
    print(stabilize_rgb_color([220, 225, 100]))
    print(stabilize_rgb_color([55, 75, 120, 1]))
    
    # match based on a class
    print(stabilize_rgb_color(Color(255, 255, 0)))
    print(stabilize_rgb_color(StabilizedColor(100, 100, 1, 1)))
    
    
    from collections import namedtuple
    
    BlackAndWhite = namedtuple('BlackAndWhite', ['r', 'g', 'b'])
    print(stabilize_rgb_color(BlackAndWhite(255, 255, 255)))
    

We can pass heterogeneous data types like lists, tuples, Color class, StabilizedColor class, namedtuple, and every other object that will contain at least three numerical data fields and get a StabilizecColor object in return. What if we try to do the same without match expression and use good ol' if/elif/else? It gets messy:


def stabilize_rgb_color(color) -> StabilizedColor:
    if (
        isinstance(color, (list, tuple))
        and len(color) >= 3
        and isinstance(color[0], int)
        and isinstance(color[-1], int)
    ):
        if len(color) > 3:
            return StabilizedColor(
                color[0], color[1], color[2], color[3]
            )
        else:
            return StabilizedColor(
                color[0], color[1], color[2], 0
            )
    elif isinstance(color, Color):
        return StabilizedColor(
            color.red, color.green, color.blue, 0
        )
    elif isinstance(color, StabilizedColor):
        return color
    # Some other elif's for each Data Type we want to cover
    # elif...
    # elif...
    else:
        raise TypeError("Only supports RGB color")

There are just too many things we need to evaluate and check before deciding what to do. What if our function goes beyond lists and tuples and expects some custom data classes? we need to add elif statement for each case and the function gets cumbersome fast.

Summary

Match case not only is more readable in many instances, but it can also handle complex situations with grace, and evaluate various patterns. Its syntax is very intuitive, as it borrows many aspects from other languages but maintains pythonic sway. This is a great addition to the language and we should be excited about it.

Enjoy the article?

Give me a clap and post a comment

Views: 302, Comments: 1 Add Comment