Structural Pattern Matching
00:00
In the previous lesson, I showed you the improved error messages in Python 3.10. In this lesson, I’ll introduce you to the new pattern matching syntax. Python 3.10 has introduced two new keywords match
and case
that, when combined, give you a new way of doing conditional code blocks.
00:18
This feature is intended to be used instead of certain kinds of long if ... else
blocks. If you’ve coded in a language that supports switch
statements, the base case of pattern matching will feel familiar.
00:30 It does go far beyond that, though. Let me start out with the simplest situation.
00:57
This function contains a single match ... case
statement that looks at the contents of the name
variable and tries to match it. If name
contains "Guido"
, then the first case gets executed.
01:10
The underscore (_
) acts as a wildcard here, similar to the default
statement in other languages, so if the name
is anything but "Guido"
, then the second case gets run.
01:21
One big difference between Python’s match ... case
syntax and the switch
statement from other languages is that the cases do not drop through. Only the first matching case is run.
01:32
After that, you leave the block. If you’ve coded in other languages with switch
statements, this means you won’t need the break
in each case. Let me run the code.
01:45 Here’s a non-Guido name. The second case was run.
01:51
And now with "Guido"
, the first case gets run. This simple use of a match ... case
statement is called literal matching, as the case
statements are matching string literals.
02:02 Let’s look at something a little more complicated.
02:08
In the top window, you’ll find a short program that uses a data class called Card
that contains two strings: a rank and a suit. The is_red()
function uses the new match
statement.
02:20
Line 11 compares the object passed in with an instance of the Card
class. If what is passed in is a card, then the suit value of the card is assigned to a variable called suit
.
02:32
This case has what is called a guard statement: the if
block just after the class on the same line. Line 11 translates into “The variable card
, small c
, is a Card
class, capital C
, that has an attribute called .suit
, whose value is either a heart or a diamond.” Line 13 matches any instance of the Card
class.
02:56
Since line 11 is checked before line 13, this will be the clubs and spade cases. Line 15 is a default catchall, which should only trigger if the content of the card
variable being matched does not contain a Card
object. Let’s play with this code just a little bit.
03:15
First, I’ll import Card
and the function.
03:25
Here, I’ve called is_red()
with the 2 of hearts. The first case gets run, and True
is returned. Hearts is red.
03:37
Now I’ve tried the king of clubs, and False
is expected and returned. And finally,
03:47
if I call it with something that isn’t a Card
class, then the default case gets called and a ValueError
is raised.
03:57
Here’s another situation where you can use pattern matching. This is a function that sums up the numbers in a list. Line 4 is the empty list case, returning 0
on line 5.
04:08
The sum of nothing is zero. Line 6 is clever. It captures the first item in the list and then uses the star operator (*
) to capture the rest of the list. Line 7 then adds the value captured in first
to the result of a recursive call to sum_list()
, passing in as the argument to the recursive call the remaining part of the list. Let’s try this out.
04:34 First, I’ll import the function.
04:39
Then, I’ll call it with a list. 1
, 3
, and 9
—lucky 13
it is. Now let’s misbehave a bit and call it without a list.
04:51
Notice that nothing comes back. No case
pattern matched, so the function simply returned after the pattern matching statement. With no explicit return call, the function returned None
. With a bit of thought, a safer function can be created. Let’s give it a try.
05:11
The first improvement here is to add some type information to the pattern being matched on line 6. This says when first
is captured, it can be an integer or a float, and the remainder of the list goes in rest
like before.
05:26
Line 8 adds a default case that raises a ValueError
if you passed in something that wasn’t a list or if your list contained anything that wasn’t an int or a float.
05:36 Let’s see how this worked out.
05:42 Let me sum some numbers.
05:51 And there’s the case from before that did nothing. This is better than nothing. As you saw in the last lesson, the right error message can make all the difference.
06:01 The not-a-list case works. Let’s try a list with non-numbers.
06:08 That’s much better, a much safer version of the function.
06:15 The FizzBuzz test is a common intro-to-coding challenge and often shows up in coding interviews. The requirement is to write a function that takes a number.
06:24
If the number’s a factor of 3
and 5
, then print "fizzbuzz"
. If the number’s only a factor of 3
, print "fizz"
.
06:32
If it is only a factor of 5
, print "buzz"
. Finally, if it is neither, print the number. There are many ways to implement this code, and that is one of the reasons it pops up in coding interviews.
06:44 Not only does it show that the candidate at least understands basic conditional statements, it can also lead to a conversation around optimization, why you chose this implementation, et cetera.
06:55
This particular implementation is pretty straightforward. Two variables are created containing the number modded with 3
and the number modded with 5
. A condition checks if the number’s a factor of both 3
and 5
, then just the factor of 5
, then the factor of 3
, and then the default. Before transforming this into a match ... case
statement, let’s make sure it works.
07:24
The function’s been imported. Let me call it with 3
. How about 14
? 15
, and 65
. Looks good! Let’s try the pattern matching version instead.
07:44
Like before, the code starts out by calculating mod 3
and mod 5
of the number passed in. The first pattern attempted is where both mod 3
and 5
are 0
, the "fizzbuzz"
case.
07:58
The second pattern is any value for mod_3
and mod_5
being 0
. Remember that this case only gets checked if the first one failed, so mod_3
can’t be 0
if you get here. This is the "buzz"
case.
08:13
The third pattern is the reverse. mod_3
must be 0
, but mod_5
can be anything else. That’s the "fizz"
case.
08:21
Then, finally, the default condition. Honestly, this isn’t much different from the if ... else
block, but it does show how you can do pattern matching across multiple variables, creating some fairly complicated conditions.
08:36 As a final demonstration, let’s combine some of these concepts together. This is a sort function using pattern matching. It expects a list of items passed in and returns a sorted version of the list.
08:49
The first pattern here checks for empty lists and lists containing a single item. The underscore (_
) here is the wildcard for any contents.
08:58 An empty list or a list with one item in it is already sorted, so just return the list itself. The second pattern checks for two items in the list and has a guard condition.
09:10
This pattern will match if the list is two items long and the first one is less than or equal to the second. The third pattern is also two items. As this pattern only fires if the one before failed, y
must be greater than x
.
09:26 The next pattern is a three-item case. It has a guard for the situation where the items are already sorted. After that, the reverse is checked for. And finally, here’s the case that handles everything else.
09:41
This case captures the first item in the list into a variable named p
and puts the remainder of the list into rest
. Two new lists are then created, one with all the items less than or equal to p
, and the second with all the items greater than p
. Each of these lists are then passed recursively to psort()
.
10:02
Finally, the sorted smaller items list, p
, in a list itself and the sorted larger items list are combined into the result. Hopefully, this gives you an idea of the power of the new match ... case
statements.
10:17 Patterns can contain multiple variables to capture, guards, and default values. In fact, that’s not all there is. There’s even still more. The pattern matching syntax supports some fairly complicated use cases.
10:32
Here is a case
statement that is capturing the value of the "age"
key inside a dictionary inside a list inside of a dictionary.
10:42 Pretty much anything you can dream up can be pattern matched as deep as you want to go.
10:48
In the cards example, you saw the simple case of matching the .suit
attribute inside of a Card
object. This kind of matching can also be arbitrarily complex.
10:58
Consider a mouse click event that takes a tuple as an argument, the point on the screen where the user clicked. The example case here captures the point tuple and assigns its pieces into x
and y
variables.
11:11
If your class being matched is a data class, then the order of items in the class specify how this matching happens. There’s also some meta information you can attach to a non data class to specify the order of matching like this, but I’ll leave that to you to dig out of the docs if you’re curious. Pattern statements also support the as
keyword similar to context managers. In this situation, it is storing the matched item into k
for use in the case
statement.
11:38
This example is overly simple—k
is going to be the same thing being matched upon—but if you create a complex match, it allows you to capture it for use within the case.
11:49 The syntax of capturing a match means that you can’t pattern match against the contents of a variable. Any use of a variable will be treated as a capture target, overwriting the variable with the item being matched.
12:02 There is an exception to this, though—anything with dot notation. Enumerations or class attributes are treated as literals. This concept is probably a little clearer with some code.
12:15
Take a look at this statement. The first case is treated like a literal because person.full_name
is using the dot notation. Even though .full_name
could be a variable, because it’s an attribute of person
, it’s treated as if it’s a literal for matching purposes. By contrast, the second case, full_name
, is on its own and therefore will be treated like a capture. If this case fires, the name variable being matched will overwrite whatever was in full_name
.
12:46 All your pattern matching needs have been met. Now, it’s time to see how Python 3.10 has improved type hinting.
Become a Member to join the conversation.