Structural Pattern Matching in Python

Structural Pattern Matching in Python

  1. Introduction to Structural Pattern Matching and Its Importance
  2. Use Structural Pattern Matching in Python

Before Python 3.10, we didn’t have any built-in way to use structural pattern matching, referred to as switch-case in other programming languages. As of Python 3.10 release, we cannot use the match ... case statement to emulate the switch ... case statement.

This tutorial introduces structural pattern matching and its importance in Python. It also uses different patterns to demonstrate using the match ... case statement.

Introduction to Structural Pattern Matching and Its Importance

As of the early days of 2021, we could not use the match keyword in released Python versions that are less than or equal to 3.9. At that time, we were used to simulating switch ... case using a dictionary or nested if/elif/else statements.

But, Python 3.10 has introduced a new feature known as structural pattern matching (match ... case statement). It is equivalent to a switch ... case statement like we have in Java, C++, and many other programming languages.

This new feature has enabled us to write simple, easy-to-read, and minimal-to-prone error flow control statements.

Use Structural Pattern Matching in Python

Structural pattern matching is used as a switch ... case statement and is more powerful than this. How? Let’s explore some examples below to learn their uses in different situations.

Basic Use of the match ... case Statement

Example Code:

colour = "blue"
match colour:
    case "green":
        print("The specified colour is green")
    case "white":
        print("Wow, you've picked white")
    case "green":
        print("Great, you are going with green colour")
    case "blue":
        print("Blue like sky...")

OUTPUT:

Blue like sky...

Here, we first have a variable colour containing blue. Then, we use the match keyword, which matches the value of the colour variable with various specified cases where each case starts with the case keyword followed by a pattern we want to compare or check.

The pattern can be one of the following:

  • literal pattern
  • capture pattern
  • wildcard pattern
  • constant value pattern
  • sequence pattern
  • mapping pattern
  • class pattern
  • OR pattern
  • walrus pattern

The match ... case statement only runs the code under the first case that matched.

What if no case is matched? How will the user know about it? For that, we can have a default case as follows.

Example Code:

colour = "yellow"
match colour:
    case "green":
        print("The specified colour is green")
    case "white":
        print("Wow, you've picked white")
    case "green":
        print("Great, you are going with green colour")
    case "blue":
        print("Blue like sky...")
    case other:
        print("No match found!")

OUTPUT:

No match found!

Use match ... case to Detect and Deconstruct Data Structures

Example Code:

student = {
     "name": {"first": "Mehvish", "last": "Ashiq"},
     "section": "B"
}

match student:
    case {"name": {"first": firstname}}:
        print(firstname)

OUTPUT:

Mehvish

In the above example, the structural pattern matching is in action at the following two lines of code:

match student:
    case {"name": {"first": firstname}}:

We use the match ... case statement to find the student’s first name by extracting it from the student data structure. Here, the student is a dictionary containing the student’s information.

The case line specifies our pattern to match the student. Considering the above example, we look for a dictionary with the "name" key whose value is a new dictionary.

This nested dictionary contains a "first" key whose value is bound to the firstname variable. Finally, we use the firstname variable to print the value.

We have learned the mapping pattern here if you observe it more deeply. How? The mapping pattern looks like {"student": s, "emails": [*es]}, which matches mapping with at least a set of specified keys.

If all sub-patterns match their corresponding values, then it binds whatever a sub-pattern bind during matching with values corresponding to the keys. If we want to allow capturing the additional items, we can add **rest at the pattern’s end.

Use match ... case With the Capture Pattern & Sequence Pattern

Example Code:

def sum_list_of_numbers(numbers):
    match numbers:
        case []:
            return 0
        case [first, *rest]:
            return first + sum_list_of_numbers(rest)

sum_list_of_numbers([1,2,3,4])

OUTPUT:

10

Here, we use the recursive function to use capture pattern to capture the match to the specified pattern and bind it to the name.

In this code example, the first case returns 0 as a summation if it matches with an empty list. The second case uses the sequence pattern with two capture patterns to match the lists with one of multiple items/elements.

Here, the first item in a list is captured & bound to the first name while the second capture pattern, *rest, uses unpacking syntax to match any number of items/elements.

Note that the rest binds to the list having all the items/elements of numbers, excluding the first one. To get the output, we call the sum_list_of_numbers() function by passing a list of numbers as given above.

Use match ... case With the Wildcard Pattern

Example Code:

def sum_list_of_numbers(numbers):
    match numbers:
        case []:
            return 0
        case [first, *rest]:
            return first + sum_list_of_numbers(rest)
        case _:
            incorrect_type = numbers.__class__.__name__
            raise ValueError(f"Incorrect Values. We Can only Add lists of numbers,not {incorrect_type!r}")

sum_list_of_numbers({'1':'2','3':'4'})

OUTPUT:

ValueError: Incorrect Values. We Can only Add lists of numbers, not 'dict'

We have learned the concept of using the wildcard pattern while learning the basic use of the match ... case statement but didn’t introduce the wildcard pattern term there. Imagine a scenario where the first two cases are not matched, and we need to have a catchall pattern as our final case.

For instance, we want to raise an error if we get any other type of data structure instead of a list. Here, we can use _ as a wildcard pattern, which will match anything without binding to the name. We add error handling in this final case to inform the user.

What do you say? Is our pattern good to go with? Let’s test it by calling the sum_list_of_numbers() function by passing a list of string values as follows:

sum_list_of_numbers(['1','2','3','4'])

It will produce the following error:

TypeError: can only concatenate str (not "int") to str

So, we can say that the pattern is still not foolproof enough. Why? Because we pass list type data structure to the sum_list_of_numbers() function but have string type values, not int type as we expected.

See the following section to learn how to resolve it.

Use match ... case With the Class Pattern

Example Code:

def sum_list_of_numbers(numbers):
    match numbers:
        case []:
            return 0
        case [int(first), *rest]:
            return first + sum_list_of_numbers(rest)
        case _:
            raise ValueError(f"Incorrect values! We can only add lists of numbers")

sum_list_of_numbers(['1','2','3','4'])

OUTPUT:

ValueError: Incorrect values! We can only add lists of numbers

The base case (the first case) returns 0; therefore, summing only works for the types we can add with numbers. Note that Python does not know how to add text strings and numbers.

So, we can use the class pattern to restrict our pattern to match integers only. The class pattern is similar to the mapping pattern but matches the attributes instead of the keys.

Use match ... case With the OR Pattern

Example Code:

def sum_list_of_numbers(numbers):
    match numbers:
        case []:
            return 0
        case [int(first) | float(first), *rest]:
            return first + sum_list_of_numbers(rest)
        case _:
            raise ValueError(f"Incorrect values! We can only add lists of numbers")

Suppose we want to make the sum_list_of_numbers() function work for a list of values, whether int-type or float-type values. We use the OR pattern represented with a pipe sign (|).

The above code must raise the ValueError if the specified list contains values other than int or float type values. Let’s test considering all three scenarios below.

Test 1: Pass a list having int type values:

sum_list_of_numbers([1,2,3,4]) #output is 10

Test 2: Pass a list having float type values:

sum_list_of_numbers([1.0,2.0,3.0,4.0]) #output is 10.0

Test 3: Pass a list having any other type excluding int and float types:

sum_list_of_numbers(['1','2','3','4'])
#output is ValueError: Incorrect values! We can only add lists of numbers

As you can see, the sum_list_of_numbers() function works for both int and float type values due to using the OR pattern.

Use match ... case With the Literal Pattern

Example Code:

def say_hello(name):
    match name:
        case "Mehvish":
            print(f"Hi, {name}!")
        case _:
            print("Howdy, stranger!")
say_hello("Mehvish")

OUTPUT:

Hi, Mehvish!

This example uses a literal pattern that matches the literal object, for instance, an explicit number or string, as we already did while learning the basic use of the match ... case statement.

It is the most basic type of pattern and lets us simulate a switch ... case statement similar to Java, C++, and other programming languages. You can visit this page to learn about all the patterns.

Mehvish Ashiq avatar Mehvish Ashiq avatar

Mehvish Ashiq is a former Java Programmer and a Data Science enthusiast who leverages her expertise to help others to learn and grow by creating interesting, useful, and reader-friendly content in Computer Programming, Data Science, and Technology.

LinkedIn GitHub Facebook