The new Python match
Diversions
Exploring the new Python match statement, Python's implementation of switch/case.
If you've decided to learn Python [1] and have any experience with other programming languages, you'll quickly notice that the ubiquitous switch
statement is nowhere to be found. Soon, though, that will no longer be the case (sort of). Python 3.10 is slated to be released in October 2021 and includes the new match
command [2] [3].
Switcheroo
The function of switch
is akin to trying to find a particular office in an office building. As you walk down the hallway, you look at each door to see if it displays the number or name in which you are interested. When you find it, you stop searching and go inside. In C (and indeed many other languages) switch
allows you to compare a value against a set of others (Listing 1).
Listing 1
switch and case in C
01 x=3 02 switch ( x ) 03 { 04 case 1: 05 { 06 printf ( "x is 1" ); 07 } 08 break; 09 case 2: 10 { 11 printf ( "x is 2" ); 12 } 13 break; 14 case 3: printf ( "x is 3" ); 15 break; 16 default: printf ( "x is not an expected value" ); 17 }
The switch
starts the compare operation, with the value you want to check passed in. The sets of curly braces then contain your case
statements.
Each case
is a value against which to check, so in this example, case 1:
says "if the value is 1, run this block of code." The program steps through each case
statement until it finds a match and then runs the code inside that block. Here, I've only put in a single print statement, but the code block can contain as many statements as you want. When the program encounters a break
, it exits the switch
statement, and the program moves on to whatever follows the switch
block.
You'll also notice default:
near the bottom of the example, which runs if none of the other case
statements match. Although not required, if default
is not there and no case
matches the switch
value, your program will move on to whatever follows the switch
statement without running anything for that block.
Python match
Until now, Python didn't really have a counterpart to the switch
statement, so you had to employ a string of if
and elif
statements:
x = 3 if x == 1: print ( "1" ) elif x == 2: print ( "2" ) elif x == 3: print ( "3" ) else: print ( "Not 1, 2, or 3" )
The same code with match
is:
x = 3 match x: case ( 1 ): print ( "1" ) case ( 2 ): print ( "2" ) case ( 3 ): print ( "3" ) case _: print ( "Not 1, 2 or 3" )
The match x
says "here's the value to find." Unlike C, the initial value passed in is not in parentheses, just the case values for comparison. Like anything else in Python, code indented under the case
is the block that will be executed, and once a block is executed, the match
happens without the need for a break
statement.
The case _
statement is Python's default case, and if used, it must be the last case
to appear in the match
. If none of the other case
blocks execute, the case _
block executes before the match
completes.
But Wait, There's More!
These examples just scratch the surface of what match
can do: It also can check variable types, shapes, and variable definitions. The example in Listing 2 [4] converts two- and three-dimensional points.
Listing 2
example1.py
01 class Point3D: 02 x = 0 03 y = 0 04 z = 0 05 __match_args__ = ( "x" , "y" , "z" ) 06 07 def __init__ ( self , x , y , z ): 08 self.x = x 09 self.y = y 10 self.z = z 11 12 def print ( self ): 13 print ( "3D Point ( {0} , {1} , {2} )".format ( self.x , self.y , self.z ) ) 14 15 class Point2D: 16 x = 0 17 y = 0 18 __match_args__ = ( "x" , "y" ) 19 20 def __init__ ( self , x=None , y=None ): 21 self.x = x 22 self.y = y 23 24 def make3Dpoint ( pt ): 25 match pt: 26 case ( x , y ): 27 return Point3D ( x , y , 0 ) 28 case ( x , y , z ): 29 return Point3D ( x , y , z ) 30 case Point2D ( x , y ): 31 return Point3D ( x , y , 0 ) 32 case Point3D ( _ , _ , _ ): 33 return pt 34 35 pointList = list() 36 pointList.append ( ( 2 , 3 ) ) 37 pointList.append ( ( 2 , 3 , 4 ) ) 38 pointList.append ( Point2D ( 2 , 3 ) ) 39 pointList.append ( Point3D ( 2 , 3 , 4 ) ) 40 41 for pt in pointList: 42 threeD = make3Dpoint ( pt ) 43 threeD.print()
The two classes Point3D
and Point2D
define three- and two-dimensional points, respectively. The 3D example defines x
, y
, and z
, and the __init__
function accepts these arguments and assigns the incoming variables to their class equivalents.
The __match_args__
in lines 5 and 18 are part of the new proposal. Usually, it will be generated automatically, but because I'm working with an alpha release (see the"Alpha Version" box), I had to define it myself. Here, match
finds positional arguments for comparison. Line 5 proposes a tuple of strings that line up with the argument names that __init__
is collecting. Point2D
is identical, except all references to z
have been removed, and it does not have a print
method.
Alpha Version
The examples in this article are based on an alpha release of Python [5]. I specifically installed this version to work with the new match
syntax, but it should not be used in production. If you want to install an alpha version for testing on Ubuntu, use the following steps (for other versions, consult your package manager):
sudo add-apt-repository §§ ppa:deadsnakes/ppa sudo apt update sudo apt install python3.10
When you add the repository, you'll be prompted for your password. The update
command updates the package list and includes options from the Python repository added in the previous line. Answer Yes to the final command and the alpha version is installed.
You can find the new version of Python by typing whereis python3.10
. I did not add the alpha version to my path, so I had to run it directly with /usr/bin/python3.10
.
Alpha versions change daily, might not be fully functional, and could have features changed or removed completely before release, so code at your own risk.
Just in Case
Lines 24-33 define make3Dpoint
, where the match
statement resides, in this case to pt
. The first case
statement on line 26 is looking for two variables in a tuple. If a tuple with two elements is received, this case matches, and x
and y
are assigned to the variables of the tuple. The function closes by returning a Point3D
instance, passing x
and y
, along with
, because a z
value was not received.
The second case
statement on line 28 works the same way, except it matches a three-member tuple. The third case
statement on line 30 begins to get even more interesting. Here, the case
matches if pt
is an instance of Point2D
and x
and y
are defined. In this case, x
and y
will be assigned to their values from the instance of Point2D
that is currently being evaluated; then, a Point3D
is created with the existing x
and y
, with z
as
. This x
and y
mapping derives from __match_args__
.
The final case
statement checks for an instance of Point3D
and has three positional arguments. An underscore (_
) is a match
wildcard. In that case, it has already been handed a Point3D
, so it just returns it unmodified.
Line 35 defines pointList
as a list
, and then lines 36-39 append
different sized tuples and class instances to the list of things to be processed. Line 41 loops over pointList
; line 42 calls make3Dpoint
which has the match
statement; and line 43 calls the print
method on the resulting 3D point (Figure 1).
Nine lines of code have normalized four different styles of input and received uniform objects. With this new syntax, you can take all sorts of input and pass them through a single function that returns consistent objects for further work.
Guards
The match
statement also includes an additional check called a guard, which is a conditional statement that can further evaluate the input being checked in a case
. Consider the "guess a number" game (Listing 3). Here, the program chooses a number between 0 and 100, and you must figure it out in as few guesses as possible.
Listing 3
numberGuess.py
01 import random 02 03 target = random.randint ( 0 , 100 ) 04 guessCount = 0 05 gameOver = False 06 07 def checkGuess ( guess ): 08 global target 09 global guessCount 10 global gameOver 11 12 match guess: 13 case ( i ) if i < 0 or i > 100: 14 print ( "Guess out of range, game is from 0 to 100" ) 15 case ( i ) if i < target: 16 print ( "Higher!" ) 17 guessCount += 1 18 case ( i ) if i > target: 19 print ( "Lower!" ) 20 guessCount += 1 21 case ( i ) if i == target: 22 print ( "Got it!" ) 23 gameOver = True 24 25 while gameOver == False: 26 print ( "You've guessed {0} times".format ( guessCount ) ) 27 guess = int ( input ( "Your guess:" ) ) 28 checkGuess ( guess )
The code begins with an import
of the random
library and then creates the random integer target
between 0 and 100 and initializes a couple of other variables: guessCount
, the number of guesses that have been made, and gameOver
, which indicates whether the game is finished.
Guesses
The checkGuess
function (lines 7-23) contains the match
statement, but first, lines 8-10 use the global
keyword to bring in the variables defined earlier so they are accessible inside the function without passing them in each time.
Line 12 starts the match
on guess
, which is the player's guess. Notice that all of the case
statements use i
instead of guess
, and because match
includes variable assignment, guess
becomes i
.
You'll also notice that each case
checks i
but then is followed by if
. These if
statements are called guards, and they create extra conditions for the item to match. The first case
(line 13) checks whether i
is less than 0 or greater than 100. If so, a Guess out of range message is displayed.
The second (line 15) and third (line 18) case
statements determine whether the guess is too high or too low, display an appropriate message, increment guessCount
, and exit the match
.
The last case
(line 21) checks to see whether the correct number has been guessed. If so, it prints the Got it! message and sets gameOver
to True
to exit the main loop.
Line 25 sets up the main loop (Figure 2), watching for gameOver
to become True
. Until then, it prints the number of guesses (line 26), asks for a guess (line 27), and checks it with the checkGuess
function (line 28).
Card Game
The next example is a card game that brings together everything discussed so far (Listing 4). The hand of cards is defined in the program as the ace of spades, jack of clubs, 2 and 9 of hearts, and 5, 6, and 9 of diamonds (lines 22-28).
Listing 4
cards.py
01 from dataclasses import dataclass 02 03 @dataclass 04 class playingCard: 05 rank: int 06 # 11 - Jack 07 # 12 - Queen 08 # 13 - King 09 # 14 - Ace 10 suit: str 11 12 def cardName ( card ): 13 match card: 14 case playingCard ( rank , suit ) if rank <= 10: 15 print ( "{0} of {1}".format ( rank , suit ) ) 16 case playingCard ( rank = 11 , suit = _ ): print ( "Jack of " + suit ) 17 case playingCard ( rank = 12 , suit = _ ): print ( "Queen of " + suit ) 18 case playingCard ( rank = 13 , suit = _ ): print ( "King of " + suit ) 19 case playingCard ( rank = 14 , suit = _ ): print ( "Ace of " + suit ) 20 21 deck = list() 22 deck.append ( playingCard ( 14 , "Spades" ) ) 23 deck.append ( playingCard ( 11 , "Clubs" ) ) 24 deck.append ( playingCard ( 2 , "Hearts" ) ) 25 deck.append ( playingCard ( 9 , "Hearts" ) ) 26 deck.append ( playingCard ( 5 , "Diamonds" ) ) 27 deck.append ( playingCard ( 6 , "Diamonds" ) ) 28 deck.append ( playingCard ( 9 , "Diamonds" ) ) 29 30 print ( "-- Face Cards --" ) 31 for card in deck: 32 match card: 33 case playingCard ( rank , suit ) if rank >= 11: 34 cardName ( card ) 35 36 print ( "-- Hearts -- " ) 37 for card in deck: 38 match card: 39 case playingCard ( rank , suit = "Hearts" ): 40 cardName ( card ) 41 42 print ( "-- Nines --" ) 43 for card in deck: 44 match card: 45 case playingCard ( rank = 9 ): 46 cardName ( card ) 47 48 print ( "-- Odd numbered non-face cards --" ) 49 for card in deck: 50 match card: 51 case playingCard ( rank ) if rank % 2 == 1 and rank <= 10: 52 cardName ( card ) 53 54 print ( "-- Odd numbered Diamonds --" ) 55 for card in deck: 56 match card: 57 case playingCard ( rank , suit = "Diamonds" ) if rank % 2 == 1 and rank <= 10: 58 cardName ( card )
To begin, the program imports dataclass
, which is a helper class that handles some basic definitions. Adding the @dataclass
decorator before the definition (line 3) generates the __init__
function automatically and assigns attributes of the same name to class variables. Therefore, in lines 22-28, where a hand of cards is defined, the values are automatically assigned to rank
and suit
. The match
already knows how to work with a dataclass
, so __match_args__
does not have to be defined, as in an earlier example. The variables rank
and suit
are defined in lines 5 and 10.
Card Names
The cardName
function uses a match
(line 13) to generate a human-readable string of the card name. Line 14 checks to see whether both rank
and suit
are defined in a case
statement. If both are present, the program checks for a numerical card with the guard if rank <= 10
. If it matches, the print
outputs the string {0} of {1}
, where format
replaces 0 with rank
and 1 with suit
.
The rest of the case
statements (lines 16-19) check for a particular rank
by specifying =11
, =12
, and so on. The suit = _
element says that suit
must be defined in the instance, but it doesn't matter what value it holds. Each case
then prints the name of the card and its suit. Note that you can check for a particular value just by specifying it in a case
.
Each of the remaining code blocks uses different combinations of case
and guards to extract different cards from the player's hand.
Card Matching
The match
in lines 32-34 checks that a rank
and suit
is present and then uses the guard if rank >= 11
to return only cards ranked 11 or higher.
The -- Hearts --
print block (lines 36-40) only has to specify suit = "Hearts"
in the case
statement for all of the hearts in the hand. The -- Nines --
case (lines 42-46) only specifies rank = 9
. Optionally it could specify suit
with no condition to make sure it is defined.
The non-face cards section (lines 48-52) is a complex condition, but it is still just a single line in the guard. It requests rank
and then uses rank % 2
to check for a remainder. If true, then the card is odd. It also checks rank <= 10
so that only non-face cards are returned.
Finally, the program specifies the suit
so only diamonds are matched (lines 54-58). The guard if rank % 2 == 1 and rank <= 10
checks for an odd number as described before and then eliminates all cards that are not numbers (Figure 3).
Conclusion
The match
statement included with Python 3.10 is slated to be release in October 2021. As you can see from the few examples I explored here, it can streamline a number of tasks and eliminate many, many if
/elif
blocks. If you start learning this new syntax now, you'll be well on your way to even more functional Python code as soon as this release becomes official!
Infos
- Python: http://www.python.org
- Python PEP 622 original match proposal: https://www.python.org/dev/peps/pep-0622/
- Python PEP 634 (replaces PEP 622): https://www.python.org/dev/peps/pep-0634/
- Code for this article: ftp://ftp.linux-magazine.com/pub/listings/linux-magazine.com/249/
- Python 3.10 alpha: https://www.python.org/downloads/release/python-3100a6/