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 switch
—Structural 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'smatch
does not have fallthrough.
Once acase
is finished, it exits thematch
scope instead of executing the nextcase
.
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:
- The pattern matches successfully → Execute the Guard
- The pattern does not match → Do not execute the Guard
- The Guard result is
True
→ Execute thecase
- The Guard result is
False
→ Skip thecase
and check the nextcase
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
, 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
- Match value is a Sequence
-
len(value) == len(patterns)
- Compare from left to right
-
Variable length (e.g.,
[first, *middle, last]
)- Match value is a Sequence
- If the sequence length is less than the number of non-
*
(star pattern) elements → match fails -
Match the non-
*
(star pattern) part first (like fixed-length matching, i.e., thefirst
part) - If the previous step succeeds, subtract the
last
part and collect the remaining elements (which become alist
, corresponding to*middle
) -
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 key
s; otherwise, it will raise a SyntaxError
.
The concrete match flow:
- The match value is a mapping.
- Every key in the pattern must exist in the match value.
- 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 :
- Check if the match value is a builtin type.
- Check if the match value is an instance of the pattern using
isinstance()
. - 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:
- Check if the attribute exists in the match value.
- Check if the attribute's value is the same as in the pattern.
- If successful, proceed to the next keyword.
-
If there are positional arguments:
- Use the match value's
__match_args__
attribute to convert positional arguments into keyword arguments.
- Use the match value's
📌 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
- https://docs.python.org/3/reference/compound_stmts.html#the-match-statement
- https://peps.python.org/pep-0636/
- https://peps.python.org/pep-0634/
- https://stackoverflow.com/questions/46701063/why-doesnt-python-have-switch-case-update-match-case-syntax-was-added-to-pyt
Photo by Mae Mu on Unsplash