Preface

Many Python core developers thought that Python didn’t need to add switch-case syntax, because you could do the same thing with if/elif/else. In fact, Guido himself wasn’t interested in this syntax either, so it wasn’t until Python 3.10 that a new match-case was added.

This new syntax is called Structural Pattern Matching, and since there are a lot of new features, there are three PEPs to introduce it.

The difference between switch-case and match-case

Take a small example to compare. The following is a function that returns the corresponding type of error message by HTTP CODE, which is what we had to write in the previous example by if-determination.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def http_error(status):
    if status == 400:
        return 'Bad request'
    elif status == 401:
        return 'Unauthorized'
    elif status == 403:
        return 'Forbidden'
    elif status == 404:
        return 'Not found'
    else:
        return 'Unknown status code'

Use the match-case syntax.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def http_error(status):
    match status:
        case 400:
            return 'Bad request'
        case 401:
            return 'Unauthorized'
        case 403:
            return 'Forbidden'
        case 404:
            return 'Not found'
        case _:
            return 'Unknown status code'

match is followed by the variable to be matched, case is followed by the different conditions, and then the statement to be executed if the conditions are met. The last case is underlined to indicate the default match, so if the previous condition doesn’t match, it will be executed in this case, which is equivalent to else before.

This is actually a typical switch-case usage, if only this, I also think there is really no need to add this new syntax, on the one hand, the code is not optimized, on the other hand, the indentation is more.

But the match-case syntax can do much more than the switch-case in C/Go languages, it is actually the match-case in Scala, Erlang, etc. It supports complex pattern matching, and I will demonstrate the flexibility and pythonic nature of this new syntax in detail with several pattern examples. I will then demonstrate the flexibility and pythonicity of this new syntax in detail with several pattern examples.

Literal mode

The above example is a literal pattern that uses the basic data structures that come with Python, such as strings, numbers, booleans, and None.

1
2
3
4
5
6
7
match number:
    case 0:
        print('zero')
    case 1:
        print('one')
    case 2:
        print('two')

Capture mode

The target of an assignment that can match a singleexpression. For demonstration purposes, each example is put into a function with the variable to be matched following the match as an argument (capture_pattern.py).

1
2
3
4
5
6
7
8
def capture(greeting):
    match greeting:
        case "":
            print("Hello!")
        case name:
            print(f"Hi {name}!")
    if name == "Santa":
        print('Match')

If greeting is not null, it will be assigned to name, but note that if greeting is null, a NameError or UnboundLocalError error will be thrown because name has not been defined before.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
In : capture('Alex')
Hi Alex!

In : capture('Santa')
Hi Santa!
Match

In : capture('')
Hello!
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
Input In [4], in <cell line: 1>()
----> 1 capture('')

Input In [1], in capture(greeting)
      1 def capture(greeting):
      2     match greeting:
      3         case "":
      4             print("Hello!")
      5         case name:
      6             print(f"Hi {name}!")
----> 7     if name == "Santa":
      8         print('Match')

UnboundLocalError: local variable 'name' referenced before assignment

Sequence mode

You can use list or tuple format results in match, and you can also use first, *rest = seq pattern to unpack as in PEP 3132 - Extended Iterable Unpacking. I’ll use an example for this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
In : def sequence(collection):
...:     match collection:
...:         case 1, [x, *others]:
...:             print(f"Got 1 and a nested sequence: {x=}, {others=}")
...:         case (1, x):
...:             print(f"Got 1 and {x}")
...:         case [x, y, z]:
...:             print(f"{x=}, {y=}, {z=}")
...:

In : sequence([1])

In : sequence([1, 2])
Got 1 and 2

In : sequence([1, 2, 3])
x=1, y=2, z=3

In : sequence([1, [2, 3]])
Got 1 and a nested sequence: x=2, others=[3]

In : sequence([1, [2, 3, 4]])
Got 1 and a nested sequence: x=2, others=[3, 4]

In : sequence([2, 3])

In : sequence((1, 2))
Got 1 and 2

Note that the following conditions must be met.

  1. the first element of this match condition needs to be 1, otherwise the match fails
  2. the first case uses a list and unwrapping, the second case uses a tuple, which is actually the same as the list semantics, and the third is still a list

If the pattern after the case is a single item, you can remove the parentheses and write it like this.

1
2
3
4
5
6
7
8
def sequence2(collection):
    match collection:
        case 1, [x, *others]:
            print(f"Got 1 and a nested sequence: {x=}, {others=}")
        case 1, x:
            print(f"Got 1 and {x}")
        case x, y, z:
            print(f"{x=}, {y=}, {z=}")

But note that one of the case 1, [x, *others] cannot be removed from the brackets, and the logic of unpacking is changed by removing it, so be careful.

Wildcard mode

Use the single underscore _ to match any result, but not to bind (not to assign to a variable or variables). Example at the beginning.

1
2
3
4
5
def http_error(status):
    match status:
        ... # 省略
        case _:
            return 'Unknown status code'

The last case _ is the wildcard pattern, and of course there can be multiple matches.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
In : def wildcard(data):
...:     match data:
...:         case [_, _]:
...:             print('Some pair')
...:

In : wildcard(None)

In : wildcard([1])

In : wildcard([1, 2])
Some pair

The sequence mode mentioned earlier is also supported in _.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
In : def sequence2(collection):
...:     match collection:
...:         case ["a", *_, "z"]:
...:             print('matches any sequence of length two or more that starts with "a" and ends with "z".')
...:         case (_, _, *_):
...:             print('matches any sequence of length two or more.')
...:         case [*_]:
...:             print('matches a sequence of any length.')
...:

In : sequence2(['a', 2, 3, 'z'])
matches any sequence of length two or more that starts with "a" and ends with "z".

In : sequence2(['a', 2, 3, 'b'])
matches any sequence of length two or more.

In : sequence2(['a', 'b'])
matches any sequence of length two or more.

In : sequence2(['a'])
matches a sequence of any length.

The use of wildcards requires attention to the logical order, placing those with small ranges in front and those with large ranges in the back to prevent them from not meeting expectations.

Constant value mode

This pattern mainly matches constant values or enum values of enum modules.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
In : class Color(Enum):
...:     RED = 1
...:     GREEN = 2
...:     BLUE = 3
...:

In : class NewColor:
...:     YELLOW = 4
...:

In : def constant_value(color):
...:     match color:
...:         case Color.RED:
...:             print('Red')
...:         case NewColor.YELLOW:
...:             print('Yellow')
...:         case new_color:
...:             print(new_color)
...:

In : constant_value(Color.RED)  # Match the first case
Red

In : constant_value(NewColor.YELLOW)  # Match the second case
Yellow

In : constant_value(Color.GREEN)  # Match the third case
Color.GREEN

In : constant_value(4)  # The constant values match the second case as well
Yellow

In : constant_value(10)  # Other constants
10

Note here that since case has a binding effect, you cannot use constants like YELLOW directly, like the following:

1
2
3
4
5
6
7
YELLOW = 4


def constant_value(color):
    match color:
        case YELLOW:
            print('Yellow')

This is the wrong syntax.

Mapping mode

is actually a case followed by support for matching using a dictionary.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
In : def mapping(config):
...:     match config:
...:         case {'sub': sub_config, **rest}:
...:             print(f'Sub: {sub_config}')
...:             print(f'OTHERS: {rest}')
...:         case {'route': route}:
...:             print(f'ROUTE: {route}')
...:

In : mapping({})

In : mapping({'route': '/auth/login'})  # Match the first case
ROUTE: /auth/login

# # Match dictionaries with sub keys, values are bound to sub_config and the rest of the dictionary is bound to rest
In : mapping({'route': '/auth/login', 'sub': {'a': 1}})  # Match the second case
Sub: {'a': 1}
OTHERS: {'route': '/auth/login'}

Class mode

Any object is supported as a match after case. Let’s start with an error example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
In : class Point:
...:     def __init__(self, x, y):
...:         self.x = x
...:         self.y = y
...:

In : def class_pattern(obj):
...:     match obj:
...:         case Point(x, y):
...:             print(f'Point({x=},{y=})')
...:

In : class_pattern(Point(1, 2))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [], in <cell line: 1>()
----> 1 class_pattern(Point(1, 2))

Input In [], in class_pattern(obj)
      1 def class_pattern(obj):
      2     match obj:
----> 3         case Point(x, y):
      4             print(f'Point({x=},{y=})')

TypeError: Point() accepts 0 positional sub-patterns (2 given)

This is because for matching, the position needs to be determined , so the position parameter needs to be used to identify it.

1
2
3
4
5
6
7
8
In : def class_pattern(obj):
...:     match obj:
...:         case Point(x=1, y=2):
...:             print(f'match')
...:

In : class_pattern(Point(1, 2))
match

Another solution to this custom class that doesn’t use positional arguments for matching is to use __match_args__ to return an array of positional arguments, like this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
In : class Point:
...:     __match_args__ = ('x', 'y')
...:
...:     def __init__(self, x, y):
...:         self.x = x
...:         self.y = y
...:

In : from dataclasses import dataclass

In : @dataclass
...: class Point2:
...:     x: int
...:     y: int
...:

In : def class_pattern(obj):
...:     match obj:
...:         case Point(x, y):
...:             print(f'Point({x=},{y=})')
...:         case Point2(x, y):
...:             print(f'Point2({x=},{y=})')
...:

In : class_pattern(Point(1, 2))
Point(x=1,y=2)

In : class_pattern(Point2(1, 2))
Point2(x=1,y=2)

Point2 here uses the standard library’s dataclasses.dataclass decorator, which provides the __match_args__ property, so it can be used directly.

Combination (OR) mode

Multiple literals can be combined to represent an or relationship using |, and | can exist more than one within a case condition to represent multiple or relationships.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def or_pattern(obj):
    match obj:
        case 0 | 1 | 2: # 0,1,2 three numbers match
            print('small number')
        case list() | set():  # List or set matching
            print('list or set')
        case str() | bytes():  # String or bytes match
            print('str or bytes')
        case Point(x, y) | Point2(x, y):  # Borrowing from the previous 2 classes, one of them can match
            print(f'{x=},{y=}')
        case [x] | x:  # list and only one element or single value matches the
            print(f'{x=}')

Note that the [x] in case [x] | x is not triggered due to the matching order, and x cannot be a set, string, byte, etc., because it will not be matched in the previous conditions. Let’s try it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
In : or_pattern(1)
small number

In : or_pattern(2)
small number

In : or_pattern([1])
list or set

In : or_pattern({1, 2})
list or set

In : or_pattern('sss')
str or bytes

In : or_pattern(b'sd')
str or bytes

In : or_pattern(Point(1, 2))
x=1,y=2

In : or_pattern(Point2(1, 2))
x=1,y=2

In : or_pattern(4)
x=4

In : or_pattern({})
x={}

Also, there is no case syntax for AND relationships in Python.

The AS pattern

The AS pattern was actually the Walrus pattern in the early days, but it was later discussed that using the as keyword would give the syntax an edge.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
In : def as_pattern(obj):
...:     match obj:
...:         case str() as s:
...:             print(f'Got str: {s=}')
...:         case [0, int() as i]:
...:             print(f'Got int: {i=}')
...:         case [tuple() as tu]:
...:             print(f'Got tuple: {tu=}')
...:         case list() | set() | dict() as iterable:
...:             print(f'Got iterable: {iterable=}')
...:
...:

In : as_pattern('sss')
Got str: s='sss'

In : as_pattern([0, 1])
Got int: i=1

In : as_pattern([(1,)])
Got tuple: tu=(1,)

In : as_pattern([1, 2, 3])
Got iterable: iterable=[1, 2, 3]

In : as_pattern({'a': 1})
Got iterable: iterable={'a': 1}

It should be noted that [0, int() as i] is a subpattern, that is, it contains patterns within patterns:[0, int() as i] is a sequence pattern for case matching, and where int() as i is a subpattern, it is an AS pattern.

Subpatterns are flexible to be combined inside the match syntax.

Adding conditions to patterns

There is also support for adding if judgments (called guard) to patterns.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
In : def go(obj):
...:     match obj:
...:         case ['go', direction] if direction in ['east', 'north']:
...:             print('Right way')
...:         case direction if direction == 'west':
...:             print('Wrong way')
...:         case ['go', _] | _:
...:             print('Other way')
...:

In : go(['go', 'east'])  # Matching condition 1
Right way

In : go('west')  # Matching condition 2
Wrong way

In : go('north')  # Match default conditions
Other way

In : go(['go', 'west'])  # Match default conditions
Other way

This allows the match to be judged further, which is equivalent to achieving some degree of AND effect.

Code

The code for this article can be found in the mp project.