1. Introduction to the Problem
Suppose we have an Animal class, with the following example code:
from typing import Optional
class Animal():
def __init__(self, name:str):
self.name = name
def __str__(self)->str:
return self.name.upper()
class Cat(Animal):
pass
class Dog(Animal):
pass
class Tiger(Animal):
pass
Now we have a requirement to initialize different classes based on the passed animal name. We can handle this in several ways.
1.1 Using if Statements
In Python, the most commonly used method for multi-branch decision-making is<span>if-elif-else</span>. The example code is as follows:
def animal_factory_use_if(animal_type:str)->Optional[Animal]:
if animal_type.lower() == "cat":
return Cat(name="Cat")
elif animal_type.lower() == "dog":
return Dog(name="Dog")
elif animal_type.lower() == "tiger":
return Tiger(name="Tiger")
else:
return None
1.2 Using a Dictionary
Besides using<span>if</span> statements, we can also use a<span>dictionary</span>. The example code is as follows:
def animal_factory_use_dict(animal_type:str)->Optional[Animal]:
# Constructing dictionary data
animal_dict={
"cat":Cat(name="Cat"),
"dog":Dog(name="Dog"),
"tiger":Tiger(name="Tiger")
}
return animal_dict.get(animal_type.lower())
1.3 Using match
For this type of multi-branch decision-making, other programming languages such as Java, C#, and Go provide a<span>switch-case</span> structure in addition to if statements. Does Python have a similar structure to achieve similar functionality?
Actually, Python introduced the <span>match-case</span> feature in version <span>3.10</span>. Let’s first look at the example code:
def animal_factory_use_match_case(animal_type:str)->Optional[Animal]:
match animal_type.lower():
case "cat":
return Cat(name="Cat")
case "dog":
return Dog(name="Dog")
case "tiger":
return Tiger(name="Tiger")
case _:
return None
The match statement is followed by the variable to be matched, case is followed by different conditions, and the last underscore indicates a default match. If none of the previous cases match, this case will be executed, similar to the previous else. Isn’t it quite similar to the switch-case in Java, C#, and Go? However, the match-case can do more than switch-case. It supports more complex pattern matching. Let’s learn together.
2. match-case
2.1 Overview
To solve the problem mentioned earlier, a very powerful <span>match-case</span> syntax was introduced in <span>Python 3.10</span>, also known as Structural Pattern Matching, which is a brand new control flow statement that allows different code blocks to be executed based on the result of a value.
2.2 Basic Concepts
2.2.1 match Syntax
<span>match-case</span> syntax is similar to the <span>switch</span> statement in other programming languages, but it is more powerful, allowing different code blocks to be executed based on the result of a value.
2.2.2 Why Use match
- Conciseness: Makes multi-condition judgments clearer and more readable
- Scalability: Supports complex structure and type matching
- Powerful Functionality: Can destructure and match sequences, dictionaries, classes, etc.
2.2.3 Syntax Structure
The basic syntax structure is as follows:
match value:
case pattern1:
# Execute code block-1
case pattern2:
# Execute code block-2
case _:
# Execute default code block
<span>match value</span>: The value to be matched<span>case pattern</span>: The matching pattern<span>case _</span>: Matches any value, serving as a pre-set scenario
3. Basic Usage
3.1 Literal Value Patterns
In literal value patterns, you can use Python’s built-in data structures such as strings, numbers, booleans, and None, as shown in the example code below:
def http_error(status):
match status:
case 400:
return "Bad request"
case 404:
return "Not found"
case 418:
return "I'm a teapot"
case _:
return "Something's wrong with the internet"
Note the last code block: the variable name underscore _ is used as a wildcard and will always match successfully. If no case matches, no branch will be executed.
3.2 Multiple Pattern Matching
You can use | (which means or) to combine several literal values in one pattern, as shown in the example code below:
def http_error_combine(status):
match status:
case 401 | 403 | 404:
return "Not allowed"
case 200 | 202:
return "request successful"
case 301 | 302 | 304:
return "redirect"
case 500 | 502:
return "server error"
case _:
return "Something's wrong with the internet"
3.3 Sequence Patterns
In <span>match-case</span>, you can also use unpacking assignment to bind variables, as shown in the example code below:
def print_point(point:Tuple[int])->str:
match point:
case (0, 0):
print("Origin")
case (0, y):
print(f"Y={y}")
case (x, 0):
print(f"X={x}")
case (x, y):
print(f"X={x}, Y={y}")
case _:
raise ValueError("Not a point")
The above functionality is explained in detail as follows:
- (0, 0): Represents two literal values, which can be understood as an extension of the literal value pattern
- (0, y), (x, 0): Represents a combination of a literal value pattern and a variable, where the variable binds to one value of point
- (x, y): Captures two values, conceptually similar to unpacking assignment, i.e., (x, y) = point
In addition to using tuples in <span>match-case</span><span>, you can also use lists, as shown in the example code below:</span>
def match_sequence(sequence:Sequence):
match sequence:
case [1,2]:
print(f"Condition: [1,2] Input {sequence} Result: {[1,2]}")
case [1,(x,y)]:
print(f"Condition: [1,(x,y)] Input {sequence} Result: x={x},y={y}")
case [x,y,z]:
print(f"Condition: [x,y,z] Input {sequence} Result: x={x},y={y},z={z}")
case [x,*y,z]:
print(f"Condition: [x,*y,z] Input {sequence} Result: x={x},y={y},z={z}")
case [x,[*y,z]]:
print(f"Condition: [x,[*y,z]] Input {sequence} Result: x={x},y={y},z={z}")
case _:
print("Not a sequence")
if __name__ == "__main__":
match_sequence([1,2])
match_sequence([1,(2,3)])
match_sequence([1,{4,5}])
match_sequence([1,2,3])
match_sequence([1,2,3,4,5])
match_sequence([1,100,99,[2,3,4,5],9999])
The code execution results are as follows:
Condition: [1,2] Input [1, 2] Result: [1, 2]
Condition: [1,(x,y)] Input [1, (2, 3)] Result: x=2,y=3
Condition: [x,*y,z] Input [1, {4, 5}] Result: x=1,y=[],z={4, 5}
Condition: [x,y,z] Input [1, 2, 3] Result: x=1,y=2,z=3
Condition: [x,*y,z] Input [1, 2, 3, 4, 5] Result: x=1,y=[2, 3, 4],z=5
Condition: [x,*y,z] Input [1, 100, 99, [2, 3, 4, 5], 9999] Result: x=1,y=[100, 99, [2, 3, 4, 5]],z=9999
Some key features of the above statements are summarized as follows:
- Similar to unpacking assignment, tuple and list patterns have the same meaning and can match any sequence in actual results. The difference is that they cannot match iterators or strings.
- Sequence patterns support extended unpacking
<span>[x,*y,z]</span>and<span>(x,*y,z)</span>, and the corresponding unpacking assignment functionality is the same, while an underscore can also follow the *
The above examples may be quite complex (you probably wouldn’t challenge yourself like this in practice), so let’s look at a more commonly used example:
def match_list_sample(color:str):
match color.split():
case ["red","green","blue"]:
print(f"{color}")
case ["purple","yellow","red"]:
print(f"{color}")
case ["red"]:
print(f"{color}")
case _:
print("Not a color")
3.4 Wildcard Patterns
Wildcard patterns are more like a catch-all pattern, using an underscore to match any result without binding a variable, as shown in the example below:
def match_list_sample(color:str):
match color.split():
case ["red","green","blue"]:
print(f"{color}")
case _:
print("Not a color")
The above basic patterns can also have the following extended patterns:
def match_list_sample(color:str):
match color.split():
case ["red","green","blue"]:
print(f"{color}")
case [_,_]:
print("Not a color")
The
<span>case _</span>in wildcard patterns can also be omitted, but it does nothing if there are no matching items.
3.5 Guard Patterns
<span>match-case</span> patterns also support adding <span>if conditions</span> after the case, as shown in the example below:
def guard_sample(number:List[int]):
match number:
case [x,y] if x>0 and y>0:
print("First quadrant")
case [x,y] if x<0 and y>0:
print("Second quadrant")
case [x, y] if x < 0 and y < 0:
print("Third quadrant")
case [x, y] if x > 0 and y < 0:
print("Fourth quadrant")
case [_,_]:
print("Data error")
3.6 Dictionary Patterns
This pattern is actually similar to the previous sequence pattern, just replacing the sequence with a dictionary for matching, as shown in the example code below:
def dict_sample(config):
match config:
case {"env":env,"host":host}:
print(f"env:{env},host:{host}")
case {"env": env, **params}:
print(f"env:{env},params:{params}")
case _:
print("error")
if __name__ == "__main__":
dict_sample({"env":"Test","path":"/home/surpass","file":"surpass.xlsx"})
dict_sample({"env":"Test","host":"surpassme.net"})
The execution results are as follows:
env:Test,params:{'path': '/home/surpass', 'file': 'surpass.xlsx'}
env:Test,host:surpassme.net
The difference between matching dictionaries and sequences is that matching sequences requires the length of the sequence, while matching dictionaries only looks at the keys. If the length of the sequence is uncertain, it is recommended to use a dictionary for matching.
3.7 AS Patterns
You can use <span>as</span> to capture sub-patterns, which can be understood as an extension of multi-pattern matching, as shown in the example code below:
def as_sample(city:str):
match city:
case ("Shanghai" | "Jiangsu" | "Zhejiang" | "Anhui") as area:
print(f"I will go to East China - {area}")
case ("Hubei" | "Hunan" | "Henan" | "Jiangxi") as area:
print(f"I will go to Central China - {area}")
case ("Guangdong" | "Guangxi" | "Hainan") as area:
print(f"I will go to South China - {area}")
case _:
print("error")
if __name__ == "__main__":
as_sample("Shanghai")
as_sample("Hainan")
The execution results are as follows:
I will go to East China - Shanghai
I will go to South China - Hainan
3.8 Class Patterns
The case statement also supports class matching, as shown in the example code below:
def class_match_sample(obj:Click):
match obj:
case Click(position=(0,0),button="Enter"):
print(f"position is (0,0),button is Enter")
case Click(position=(100, 200),button="Esc"):
print(f"position is (100, 200),button is Esc")
case _:
print("error")
if __name__ == "__main__":
class_match_sample(Click((0, 0), "Enter"))
class_match_sample(Click((100,200),"Esc"))
The execution results are as follows:
position is (0,0),button is Enter
position is (100, 200),button is Esc
However, when using class patterns, the positions need to be specified, so the parameters after case need to use named keyword arguments for passing parameters, otherwise, the following error will occur:
def class_match_error_sample(obj:Click):
match obj:
case Click((0,0),"Enter"):
print(f"position is (0,0),button is Enter")
case Click((100, 200),"Esc"):
print(f"position is (100, 200),button is Esc")
case _:
print("error")
If you use the above definition, the following error message will appear at runtime:
Traceback (most recent call last):
File "C:\Users\Surpass\Documents\PyCharmProjects\TestNote\main.py", line 200, in <module>
class_match_error_sample(Click((0, 0), "Enter"))
File "C:\Users\Surpass\Documents\PyCharmProjects\TestNote\main.py", line 165, in class_match_error_sample
case Click((0,0),"Enter"):
^^^^^^^^^^^^^^^^^^^^
TypeError: Click() accepts 0 positional sub-patterns (2 given)
To solve the parameter passing issue, the official provides another method, which is to add <span>__match_args__</span> to the class to return a tuple of positional parameters, as shown in the example code below:
class Click():
__match_args__ = ("position", "button")
def __init__(self, position:Tuple[int], button:str):
self.position = position
self.button = button
def class_match_sample(obj:Click):
match obj:
case Click((0,0),"Enter"):
print(f"position is (0,0),button is Enter")
case Click((100, 200),"Esc"):
print(f"position is (100, 200),button is Esc")
case _:
print("error")
4. Precautions
4.1 Python Version Requirements
- The match syntax is only supported in Python 3.10 and above, otherwise it will cause a syntax error.
4.2 Wildcard Usage
- _ is a special pattern that matches any value.
- If you need to capture a value, you need to use a variable name.
4.3 Matching Order
- Matching is done in order, once a match is successful, subsequent cases will be ignored.
- When using, it is recommended to place more specific patterns at the front and general patterns at the back.
5. References
- https://docs.python.org/zh-cn/3.13/tutorial/controlflow.html#match-statements
- https://peps.python.org/pep-0636/#composing-patterns