Also published on my personal blog (in Chinese).
Python 3.10 | Match Statement - 更靈活的 switch

Overview

When I first started learning Python, I was surprised to find that there was no switch statement available. Fortunately, starting from Python 3.10, Python introduced its own version of switchStructural Pattern Matching.

So, what is Structural Pattern Matching? Compared to C++'s switch, I find it more similar to C#'s Pattern Matching.

Here's a simple example:

is_perform = False

match ("anon", "soyorin"):
    # Mismatch
    case 'tomorin':
        print('組一輩子樂團')

    # Mismatch: soyorin != rana
    case ("anon", "rana"):
        print('有趣的女人')

    # Successful match, but guard fails
    case  ("anon", "soyorin") if is_perform:
        print('為什麼要演奏春日影!')

    # Matches and binds y to "soyorin"
    case ("anon", y):
        print(f'愛音,{y} 強到靠北')

    # Pattern not attempted
    case _:
        print('我還是會繼續下去')


# 愛音,soyorin 強到靠北

📌 Note
Unlike C++, Python's match does not have fallthrough.
Once a case is finished, it exits the match scope instead of executing the next case.

Introduction

In the document, the part after match ("anon", "soyorin"): is called subject_expr.
For better understanding, we will call it "match value."

Guards

case if :

If we want to perform an additional check after a successful match, we can add an if condition after the pattern,

like case ("anon", "soyorin") if is_perform: in the example.

This syntax is called a Guard.

The execution flow is as follows:

  1. The pattern matches successfully → Execute the Guard
  2. The pattern does not match → Do not execute the Guard
  3. The Guard result is True → Execute the case
  4. The Guard result is False → Skip the case and check the next case

Irrefutable Case Blocks

This refers to cases that must match, similar to C++'s default.

However, it can only appear in the last case, and the entire match block can have only one such case.

As for which patterns qualify, you can refer to:

Python Documentation - Irrefutable Case Blocks

For example:

match 2:
    case 1:
        print("value is 1")
    case x:
        print("Irrefutable Case Blocks")


# Irrefutable Case Blocks

Patterns

OR Patterns

Just like its literal meaning, it works as an or.

The pattern will be tried one by one until one succeeds.

Here's a simple example:

match 1:
    case 1 | 2 | 3:
        print("value is 1 or 2 or 3")


# value is 1 or 2 or 3

AS Patterns

We used or happily earlier, but how can we extract the original value?

For this, we can use as to get the value from the previous match.

So, case as :, when the pattern is successful,

the match value will be bound to name, i.e., name = .

Continuing from the previous example:

match 1:
    case 1 | 2 | 3 as x:
        print(f"value is {x}")


# value is 1

Literal Patterns

Earlier, we used several patterns to match against literals in Python,

such as int, str, None, bool, and so on.

In simple terms, when if == , the match will succeed.

However, when encountering Singletons, like None, True, or False, we use is to perform the comparison.

Capture Patterns

Used to bind the matched value to a variable.

In the pattern, a name can only be bound once.

match (1, 1):
    # SyntaxError
    case x, x:
        print(f"Matched: {x}")


#     case x, x:
#             ^
# SyntaxError: multiple assignments to name 'x' in pattern

In the following example, when the match succeeds, "soyorin" will be bound to the variable y.

match ("anon", "soyorin"):
    # Matches and binds y to "soyorin"
    case ("anon", y):
        print(f'愛音,{y} 強到靠北')


# 愛音,soyorin 強到靠北

Wildcard Patterns

_, used to match any value, is essentially used as a default.

For example:

match ("Raana", "soyorin"):

    # Matches and binds y to "soyorin"
    case ("anon", y):
        print(f'愛音,{y} 強到靠北')

    # Pattern not attempted
    case _:
        print('我還是會繼續下去')


# 我還是會繼續下去

Value Patterns

Value Pattern ,refers to those that can be accessed through name resolution,

i.e., variables accessed via . such as enum, math.pi, etc.

They are compared using ==, like == .

Example:

from enum import Enum


class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3


match Color.RED:
    case Color.RED:
        print("RED")
    case Color.GREEN:
        print("GREEN")
    case Color.BLUE:
        print("BLUE")
    case _:
        print("unknown")


# RED

Group Patterns

Honestly, I don't think this is really a pattern;

it just tells you that you can add () to improve readability.

For example, the previous case:

match 1:
    # case 1 | 2 | 3 as x:
    case (1 | 2 | 3) as x:
        print(f"value is {x}")


# value is 1

Sequence Patterns

To match "Sequence", like List, Tuple, and so on.

But str, bytes, and bytearray will not be considered Sequence Patterns.

📌 Note

  • Python does not distinguish (...) and [...]; they are the same.
  • Another thing to note is that (3 | 4) will be a** group pattern*, but [3 | 4] is still a* sequence pattern**.

The concrete match flow is:

  • Fixed length
    1. Match value is a Sequence
    2. len(value) == len(patterns)
    3. Compare from left to right
  • Variable length (e.g., [first, *middle, last])
    1. Match value is a Sequence
    2. If the sequence length is less than the number of non-* (star pattern) elements → match fails
    3. Match the non-* (star pattern) part first (like fixed-length matching, i.e., the first part)
    4. If the previous step succeeds, subtract the last part and collect the remaining elements (which become a list, corresponding to *middle)
    5. Finally, match the remaining part, i.e., last (like fixed-length matching)

I think it will be clearer if we check some examples.

# fixed-length

match [10, 20, 30]:
    # note that this match can also bind names
    case [x, y, z]:
        print(f"x={x}, y={y}, z={z}")


# x=10, y=20, z=30
# variable-length

match [1, 2, 3, 4, 5]:
    case [first, *middle, last]:
        print(f"first={first}, middle={middle}, last={last}")


# first=1, middle=[2, 3, 4], last=5

Mapping Patterns

To match "mapping", the most common type is dict.

Like before, we can put ** (double_star_pattern) at the end to collect remaining elements.

Additionally, we cannot have duplicate keys; otherwise, it will raise a SyntaxError.

The concrete match flow:

  1. The match value is a mapping.
  2. Every key in the pattern must exist in the match value.
  3. The corresponding value for each key must be the same as in the pattern.

For example:

match {"name": "Bob", "age": 30, "city": "NY"}:
    case {"name": n, "age": a}:
        print(f"name={n}, age={a}")


# name=Bob, age=30

Class Patterns

Used to match class, but the matching flow is more complex.

Like function arguments, there are two types: positional arguments and keyword arguments.

Matching flow :

  1. Check if the match value is a builtin type.
  2. Check if the match value is an instance of the pattern using isinstance().
  3. Check if the class pattern has any arguments. If not, the match succeeds.

If it has arguments, they are split into keyword or positional arguments:

  • Only keyword arguments:
    1. Check if the attribute exists in the match value.
    2. Check if the attribute's value is the same as in the pattern.
    3. If successful, proceed to the next keyword.
  • If there are positional arguments:
    1. Use the match value's __match_args__ attribute to convert positional arguments into keyword arguments.

📌 Note

  • If object.__match_args__ is not defined, its default value is an empty tuple ().
  • Some built-in types (such as bool, int, list, str, etc.) are matched as entire objects after receiving positional arguments.

Example :

# Keyword argument
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y


match Point(1, 2):
    case Point(x=1, y=y_value):
        print(f"Matched! y={y_value}")


# Matched! y=2
# positional argument
class Point:
    # assigned a tuple of strings
    __match_args__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y


match Point(1, 2):
    # converted to keyword patterns using the __match_args__
    case Point(1, y_value):
        print(f"Matched! y={y_value}")


# Matched! y=2

Final Thoughts

If you have any questions, feel free to leave a comment below.

References

Photo by Mae Mu on Unsplash