python-course.eu

17. Structural Pattern Matching

By Bernd Klein. Last modified: 29 Mar 2023.

Introduction

Structural pattern matching is a programming technique that allows complex data structures to be applied in a concise and readable way in comparisons. Structural pattern matching is used in various programming languages, including Python.

With structural pattern matching, a data structure (such as a list or a tuple) is analyzed and searched for a specific pattern that one has previously defined. One can then perform different actions based on the patterns found. This can often be written in a concise and easy-to-understand way, which increases the readability of the code.

Patterns

In general, structural pattern recognition can reduce error-proneness and increase development productivity by shortening syntax and improving readability.

'Structural Pattern Matching' was newly introduced in Python 3.10. The syntax for this new feature was proposed in PEP 622 in June 2020. The pattern matching statement of Python was inspired by similar syntax found in Scala, Erlang, and other languages. In its simplest form it behaves like the switch statement of C, C++, or Java. Yet, there are more improved use cases for this feature, as you will learn in this tutorial. We will learn that it is possible in Python to unpack a pattern into its basic component.

Before you go on with this chapter, you have to make sure that Python version 3.10 or higher is running on your system. The easiest way to find out is to use version from sys:

import sys
sys.version

OUTPUT:

'3.10.10 (main, Mar 21 2023, 18:45:11) [GCC 11.2.0]'

In Python, structural pattern matching is introduced by the match statement.

The match statement works by comparing an evaluated expression (also known as an "expression under test") with one or more patterns. A pattern is a special expression that corresponds to a particular data type or contains a complex data structure such as a list or a tuple.

Let's look at a simple example of structural pattern matching. We write a function that responds to a chosen language, so 'chosen_language' is the expression to check:

Let us look at a simple structural pattern matching example. We write a function which reacts to a chosen language:

def greeting(language):
    chosen_language = language.capitalize()
    match chosen_language:
        case 'English':
            print('Hello')
        case 'German':
            print('Hallo!')
            
greeting('english')

OUTPUT:

Hello

In Python's structural pattern matching, the underscore character (_) is used as a placeholder to match any value. This means for the following code that the case with the '_' always applies if 'English' or 'German' is not entered:

def greeting(language):
    chosen_language = language.capitalize()
    match chosen_language:
        case 'English':
            print('Hello')
        case 'German':
            print('Hallo!')
        case _:
            print(f"Hello, I don't know {chosen_language}!")
            
greeting('english')
greeting('french')

OUTPUT:

Hello
Hello, I don't know French!

As you most probably know, German is not only spoken in Germany, but in Austria and parts of Switzerland as well. We should not forget that German is also spoken in Liechtenstein and Luxembourg, in addition to French. This example shows now a usecase in which the structural pattern matching makes things easier:

def greeting(language):
    chosen_language = language.split()
    match chosen_language:
        case ['English']:
            print('Hello')
        case ['German']:
            print('Hallo!')
        case ['German', 'AT']:
            print(f'Servus!')
        case ['German', 'CH']:
            print(f'Grüezi!')
        case ['German', 'DE']:
            print(f'Hallo')
        case ['German', 'LU']:
            print(f'Gudden Dag')
        case ['German', 'LI']:
            print(f'Grüezi')

for lang in ['English', 'German LU', 'German CH']:
    greeting(lang)

OUTPUT:

Hello
Gudden Dag
Grüezi!

Without match the Python code would be similar to the following code:

def greeting(language):
    chosen_language = language.split()
    if len(chosen_language) == 1:
        if chosen_language == ['English']:
            print('Hello')
        elif chosen_language == ['German']:
            print('Hallo!')
    elif len(chosen_language) == 2:
        if chosen_language == ['German', 'AT']:
            print(f'Servus!')
        elif chosen_language == ['German', 'CH']:
            print(f'Grüezi!')
        elif chosen_language == ['German', 'DE']:
            print(f'Hallo')
        elif chosen_language == ['German', 'LU']:
            print(f'Gudden Dag')
        elif chosen_language == ['German', 'LI']:
            print(f'Grüezi')

for lang in ['English', 'German LU', 'German CH']:
    greeting(lang)

OUTPUT:

Hello
Gudden Dag
Grüezi!

We could improve the previous code a tiny little bit, but it should be clear that the match is superior in its clarity in this case. We can further improve our example: What if somebody choses an unknown language or an unknown language plus a region ? It is very easy. Instead of using a string literal in our pattern, we use variable names:

def greeting(language):
    chosen_language = language.split()
    match chosen_language:
        case ['English']:
            print('Hello')
        case ['German']:
            print('Hallo!')
        case [unknown_language]:
            print(f"So far, we don't know how to greet in {unknown_language}!")
        case ['German', 'AT']:
            print(f'Servus!')
        case ['German', 'CH']:
            print(f'Grüezi!')
        case ['German', 'DE']:
            print(f'Hallo')
        case ['German', 'LU']:
            print(f'Gudden Dag')
        case ['German', 'LI']:
            print(f'Grüezi')
        case [lang, region]:
            print(f"Sorry, we don't know {lang} in your region {region}!")

for lang in ['English', 'German LU', 'French', 'German CH', 'English CA']:
    greeting(lang)

OUTPUT:

Hello
Gudden Dag
So far, we don't know how to greet in French!
Grüezi!
Sorry, we don't know English in your region CA!

We present an interesting use case in the form of an imaginary adventure game. Gamers could write actions as strings in the various formats, like

commands = ['go north','go west', 'drop potion', 'drop all weapons', 'drop shield']
weapons = ['axe','sword','dagger']
shield = True
inventory = ['apple','wood','potion'] + weapons + ['shield']
for command in commands: 
    match command.split():
        case ["help"]:
            print("""You can use the following commands:
            """)
        case ["go", direction]: 
            print('going', direction)
        case ["drop", "all", "weapons"]: 
            for weapon in weapons:
                inventory.remove(weapon)
        case ["drop", item]:
            print('dropping', item)
            inventory.remove(item)
        case ["drop shield"]:
            shield = False 
        case _:
            print(f"Sorry, I couldn't understand {command!r}")

OUTPUT:

going north
going west
dropping potion
dropping shield
['apple', 'wood']

In the following example, we define the factorial function using match:

def factorial(n):
    match n:
        case 0 | 1:
            return 1
        case _:
            return n * factorial(n - 1)

for i in range(6):
    print(i, factorial(i))

OUTPUT:

0 1
1 1
2 2
3 6
4 24
5 120

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Upcoming online Courses

Enrol here