Structural Pattern Matching in Python
- Introduction to Structural Pattern Matching and Its Importance
- 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:
# >= Python 3.10
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:
# >= Python 3.10
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:
# >= Python 3.10
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:
# >= Python 3.10
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:
# >= Python 3.10
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:
# >= Python 3.10
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:
# >= Python 3.10
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:
# >= Python 3.10
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:
# >= Python 3.10
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.