This PEP is a tutorial for the pattern matching introduced byPEP 634.
PEP 622 proposed syntax for pattern matching, which received detailed discussionboth from the community and the Steering Council. A frequent concern wasabout how easy it would be to explain (and learn) this feature. This PEPaddresses that concern providing the kind of document which developers could useto learn about pattern matching in Python.
This is considered supporting material forPEP 634 (the technical specificationfor pattern matching) andPEP 635 (the motivation and rationale for having patternmatching and design considerations).
For readers who are looking more for a quick review than for a tutorial,seeAppendix A.
As an example to motivate this tutorial, you will be writing a text adventure. That isa form of interactive fiction where the user enters text commands to interact with afictional world and receives text descriptions of what happens. Commands will besimplified forms of natural language likegetsword,attackdragon,gonorth,entershop orbuycheese.
Your main loop will need to get input from the user and split it into words, let’s saya list of strings like this:
command=input("What are you doing next? ")# analyze the result of command.split()
The next step is to interpret the words. Most of our commands will have two words: anaction and an object. So you may be tempted to do the following:
[action,obj]=command.split()...# interpret action, obj
The problem with that line of code is that it’s missing something: what if the usertypes more or fewer than 2 words? To prevent this problem you can either check the lengthof the list of words, or capture theValueError that the statement above would raise.
You can use a matching statement instead:
matchcommand.split():case[action,obj]:...# interpret action, obj
The match statement evaluates the“subject” (the value after thematchkeyword), and checks it against thepattern (the code next tocase). A patternis able to do two different things:
[action,obj]pattern matches any sequence of exactly two elements. This is calledmatchingaction=subject[0] andobj=subject[1].If there’s a match, the statements inside the case block will be executed with thebound variables. If there’s no match, nothing happens and the statement aftermatch is executed next.
Note that, in a similar way to unpacking assignments, you can use either parenthesis,brackets, or just comma separation as synonyms. So you could writecaseaction,objorcase(action,obj) with the same meaning. All forms will match any sequence (forexample lists or tuples).
Even if most commands have the action/object form, you might want to have user commandsof different lengths. For example, you might want to add single verbs with no object likelook orquit. A match statement can (and is likely to) have more than onecase:
matchcommand.split():case[action]:...# interpret single-verb actioncase[action,obj]:...# interpret action, obj
The match statement will check patterns from top to bottom. If the pattern doesn’tmatch the subject, the next pattern will be tried. However, once thefirstmatching pattern is found, the body of that case is executed, and all furthercases are ignored. This is similar to the way that anif/elif/elif/...statement works.
Your code still needs to look at the specific actions and conditionally executedifferent logic depending on the specific action (e.g.,quit,attack, orbuy).You could do that using a chain ofif/elif/elif/..., or using a dictionary offunctions, but here we’ll leverage pattern matching to solve that task. Instead of avariable, you can use literal values in patterns (like"quit",42, orNone).This allows you to write:
matchcommand.split():case["quit"]:print("Goodbye!")quit_game()case["look"]:current_room.describe()case["get",obj]:character.get(obj,current_room)case["go",direction]:current_room=current_room.neighbor(direction)# The rest of your commands go here
A pattern like["get",obj] will match only 2-element sequences that have a firstelement equal to"get". It will also bindobj=subject[1].
As you can see in thego case, we also can use different variable names indifferent patterns.
Literal values are compared with the== operator except for the constantsTrue,False andNone which are compared with theis operator.
A player may be able to drop multiple items by using a series of commandsdropkey,dropsword,dropcheese. This interface might be cumbersome, andyou might like to allow dropping multiple items in a single command, likedropkeyswordcheese. In this case you don’t know beforehand how many words willbe in the command, but you can use extended unpacking in patterns in the same way thatthey are allowed in assignments:
matchcommand.split():case["drop",*objects]:forobjinobjects:character.drop(obj,current_room)# The rest of your commands go here
This will match any sequences having “drop” as its first elements. All remainingelements will be captured in alist object which will be bound to theobjectsvariable.
This syntax has similar restrictions as sequence unpacking: you can not have more than onestarred name in a pattern.
You may want to print an error message saying that the command wasn’t recognized whenall the patterns fail. You could use the feature we just learned and writecase[*ignored_words] as your last pattern. There’s however a much simpler way:
matchcommand.split():case["quit"]:...# Code omitted for brevitycase["go",direction]:...case["drop",*objects]:......# Other casescase_:print(f"Sorry, I couldn't understand{command!r}")
This special pattern which is written_ (and called wildcard) alwaysmatches but it doesn’t bind any variables.
Note that this will match any object, not just sequences. As such, it only makessense to have it by itself as the last pattern (to prevent errors, Python will stopyou from using it before).
This is a good moment to step back from the examples and understand how the patternsthat you have been using are built. Patterns can be nested within each other, and wehave been doing that implicitly in the examples above.
There are some “simple” patterns (“simple” here meaning that they do not contain otherpatterns) that we’ve seen:
direction,action,objects). Wenever discussed these separately, but used them as part of other patterns.True,False, andNone)_Until now, the only non-simple pattern we have experimented with is the sequence pattern.Each element in a sequence pattern can in fact beany other pattern. This means that you could write a pattern like["first",(left,right),_,*rest]. This will match subjects which are a sequence of atleast three elements, where the first one is equal to"first" and the second one isin turn a sequence of two elements. It will also bindleft=subject[1][0],right=subject[1][1], andrest=subject[3:]
Going back to the adventure game example, you may find that you’d like to have severalpatterns resulting in the same outcome. For example, you might want the commandsnorth andgonorth to be equivalent. You may also desire to have aliases forgetX,pickupX andpickXup for any X.
The| symbol in patterns combines them as alternatives. You could for example write:
matchcommand.split():...# Other casescase["north"]|["go","north"]:current_room=current_room.neighbor("north")case["get",obj]|["pick","up",obj]|["pick",obj,"up"]:...# Code for picking up the given object
This is called anor pattern and will produce the expected result. Patterns aretried from left to right; this may be relevant to know what is bound if more thanone alternative matches. An important restriction when writing or patterns is that allalternatives should bind the same variables. So a pattern[1,x]|[2,y] is notallowed because it would make unclear which variable would be bound after a successfulmatch.[1,x]|[2,x] is perfectly fine and will always bindx if successful.
The first version of our “go” command was written with a["go",direction] pattern.The change we did in our last version using the pattern["north"]|["go","north"]has some benefits but also some drawbacks in comparison: the latest version allows thealias, but also has the direction hardcoded, which will force us to actually haveseparate patterns for north/south/east/west. This leads to some code duplication, but atthe same time we get better input validation, and we will not be getting into thatbranch if the command entered by the user is"gofigure!" instead of a direction.
We could try to get the best of both worlds doing the following (I’ll omit the aliasedversion without “go” for brevity):
matchcommand.split():case["go",("north"|"south"|"east"|"west")]:current_room=current_room.neighbor(...)# how do I know which direction to go?
This code is a single branch, and it verifies that the word after “go” is really adirection. But the code moving the player around needs to know which one was chosen andhas no way to do so. What we need is a pattern that behaves like the or pattern but atthe same time does a capture. We can do so with anas pattern:
matchcommand.split():case["go",("north"|"south"|"east"|"west")asdirection]:current_room=current_room.neighbor(direction)
The as-pattern matches whatever pattern is on its left-hand side, but also binds thevalue to a name.
The patterns we have explored above can do some powerful data filtering, but sometimesyou may wish for the full power of a boolean expression. Let’s say that you would actuallylike to allow a “go” command only in a restricted set of directions based on the possibleexits from the current_room. We can achieve that by adding aguard to ourcase. Guards consist of theif keyword followed by any expression:
matchcommand.split():case["go",direction]ifdirectionincurrent_room.exits:current_room=current_room.neighbor(direction)case["go",_]:print("Sorry, you can't go that way")
The guard is not part of the pattern, it’s part of the case. It’s only checked ifthe pattern matches, and after all the pattern variables have been bound (that’s why thecondition can use thedirection variable in the example above). If the patternmatches and the condition is truthy, the body of the case executes normally. If thepattern matches but the condition is falsy, the match statement proceeds to check thenext case as if the pattern hadn’t matched (with the possible side-effect ofhaving already bound some variables).
Your adventure is becoming a success and you have been asked to implement a graphicalinterface. Your UI toolkit of choice allows you to write an event loop where you can get a newevent object by callingevent.get(). The resulting object can have different type andattributes according to the user action, for example:
KeyPress object is generated when the user presses a key. It has akey_nameattribute with the name of the key pressed, and some other attributes regarding modifiers.Click object is generated when the user clicks the mouse. It has an attributeposition with the coordinates of the pointer.Quit object is generated when the user clicks on the close button for the gamewindow.Rather than writing multipleisinstance() checks, you can use patterns to recognizedifferent kinds of objects, and also apply patterns to its attributes:
matchevent.get():caseClick(position=(x,y)):handle_click_at(x,y)caseKeyPress(key_name="Q")|Quit():game.quit()caseKeyPress(key_name="up arrow"):game.go_north()...caseKeyPress():pass# Ignore other keystrokescaseother_event:raiseValueError(f"Unrecognized event:{other_event}")
A pattern likeClick(position=(x,y)) only matches if the type of the event isa subclass of theClick class. It will also require that the event has apositionattribute that matches the(x,y) pattern. If there’s a match, the localsx andy will get the expected values.
A pattern likeKeyPress(), with no arguments will match any object which is aninstance of theKeyPress class. Only the attributes you specify in the pattern arematched, and any other attributes are ignored.
The previous section described how to match named attributes when doing an object match.For some objects it could be convenient to describe the matched arguments by position(especially if there are only a few attributes and they have a “standard” ordering).If the classes that you are using are named tuples or dataclasses, you can do that byfollowing the same order that you’d use when constructing an object. For example, ifthe UI framework above defines their class like this:
fromdataclassesimportdataclass@dataclassclassClick:position:tuplebutton:Button
then you can rewrite your match statement above as:
matchevent.get():caseClick((x,y)):handle_click_at(x,y)
The(x,y) pattern will be automatically matched against thepositionattribute, because the first argument in the pattern corresponds to the firstattribute in your dataclass definition.
Other classes don’t have a natural ordering of their attributes so you’re required touse explicit names in your pattern to match with their attributes. However, it’s possibleto manually specify the ordering of the attributes allowing positional matching, like inthis alternative definition:
classClick:__match_args__=("position","button")def__init__(self,pos,btn):self.position=posself.button=btn...
The__match_args__ special attribute defines an explicit order for your attributesthat can be used in patterns likecaseClick((x,y)).
Your pattern above treats all mouse buttons the same, and you have decided that youwant to accept left-clicks, and ignore other buttons. While doing so, you notice thatthebutton attribute is typed as aButton which is an enumeration built withenum.Enum. You can in fact match against enumeration values like this:
matchevent.get():caseClick((x,y),button=Button.LEFT):# This is a left clickhandle_click_at(x,y)caseClick():pass# ignore other clicks
This will work with any dotted name (likemath.pi). However an unqualified name (i.e.a bare name with no dots) will be always interpreted as a capture pattern, so avoidthat ambiguity by always using qualified constants in patterns.
You have decided to make an online version of your game. Allof your logic will be in a server, and the UI in a client which will communicate usingJSON messages. Via thejson module, those will be mapped to Python dictionaries,lists and other builtin objects.
Our client will receive a list of dictionaries (parsed from JSON) of actions to take,each element looking for example like these:
{"text":"Theshopkeepersays'Ah!WehaveCamembert,yessir'","color":"blue"}{"sleep":3}{"sound":"filename.ogg","format":"ogg"}Until now, our patterns have processed sequences, but there are patterns to matchmappings based on their present keys. In this case you could use:
foractioninactions:matchaction:case{"text":message,"color":c}:ui.set_text_color(c)ui.display(message)case{"sleep":duration}:ui.wait(duration)case{"sound":url,"format":"ogg"}:ui.play(url)case{"sound":_,"format":_}:warning("Unsupported audio format")
The keys in your mapping pattern need to be literals, but the values can be anypattern. As in sequence patterns, all subpatterns have to match for the generalpattern to match.
You can use**rest within a mapping pattern to capture additional keys inthe subject. Note that if you omit this, extra keys in the subject will beignored while matching, i.e. the message{"text":"foo","color":"red","style":"bold"} will match the first patternin the example above.
The code above could use some validation. Given that messages came from an externalsource, the types of the field could be wrong, leading to bugs or security issues.
Any class is a valid match target, and that includes built-in classes likeboolstr orint. That allows us to combine the code above with a class pattern.So instead of writing{"text":message,"color":c} we can use{"text":str()asmessage,"color":str()asc} to ensure thatmessage andcare both strings. For many builtin classes (seePEP 634 for the whole list), you canuse a positional parameter as a shorthand, writingstr(c) rather thanstr()asc.The fully rewritten version looks like this:
foractioninactions:matchaction:case{"text":str(message),"color":str(c)}:ui.set_text_color(c)ui.display(message)case{"sleep":float(duration)}:ui.wait(duration)case{"sound":str(url),"format":"ogg"}:ui.play(url)case{"sound":_,"format":_}:warning("Unsupported audio format")
A match statement takes an expression and compares its value to successivepatterns given as one or more case blocks. This is superficiallysimilar to a switch statement in C, Java or JavaScript (and manyother languages), but much more powerful.
The simplest form compares a subject value against one or more literals:
defhttp_error(status):matchstatus:case400:return"Bad request"case404:return"Not found"case418:return"I'm a teapot"case_:return"Something's wrong with the Internet"
Note the last block: the “variable name”_ acts as awildcard andnever fails to match.
You can combine several literals in a single pattern using| (“or”):
case401|403|404:return"Not allowed"
Patterns can look like unpacking assignments, and can be used to bindvariables:
# point is an (x, y) tuplematchpoint: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_:raiseValueError("Not a point")
Study that one carefully! The first pattern has two literals, and canbe thought of as an extension of the literal pattern shown above. Butthe next two patterns combine a literal and a variable, and thevariablebinds a value from the subject (point). The fourthpattern captures two values, which makes it conceptually similar tothe unpacking assignment(x,y)=point.
If you are using classes to structure your datayou can use the class name followed by an argument list resembling aconstructor, but with the ability to capture attributes into variables:
fromdataclassesimportdataclass@dataclassclassPoint:x:inty:intdefwhere_is(point):matchpoint:casePoint(x=0,y=0):print("Origin")casePoint(x=0,y=y):print(f"Y={y}")casePoint(x=x,y=0):print(f"X={x}")casePoint():print("Somewhere else")case_:print("Not a point")
You can use positional parameters with some builtin classes that provide anordering for their attributes (e.g. dataclasses). You can also define a specificposition for attributes in patterns by setting the__match_args__ specialattribute in your classes. If it’s set to (“x”, “y”), the following patterns are allequivalent (and all bind they attribute to thevar variable):
Point(1,var)Point(1,y=var)Point(x=1,y=var)Point(y=var,x=1)
Patterns can be arbitrarily nested. For example, if we have a shortlist of points, we could match it like this:
matchpoints:case[]:print("No points")case[Point(0,0)]:print("The origin")case[Point(x,y)]:print(f"Single point{x},{y}")case[Point(0,y1),Point(0,y2)]:print(f"Two on the Y axis at{y1},{y2}")case_:print("Something else")
We can add anif clause to a pattern, known as a “guard”. If theguard is false,match goes on to try the next case block. Notethat value capture happens before the guard is evaluated:
matchpoint:casePoint(x,y)ifx==y:print(f"Y=X at{x}")casePoint(x,y):print(f"Not on the diagonal")
Several other key features:
collections.abc.Sequence.)[x,y,*rest] and(x,y,*rest) work similar to wildcards in unpacking assignments. Thename after* may also be_, so(x,y,*_) matches a sequenceof at least two items without binding the remaining items.{"bandwidth":b,"latency":l} captures the"bandwidth" and"latency" values from a dict. Unlike sequencepatterns, extra keys are ignored. A wildcard**rest is alsosupported. (But**_ would be redundant, so it is not allowed.)as keyword:case(Point(x1,y1),Point(x2,y2)asp2):...
True,False andNone are compared by identity.fromenumimportEnumclassColor(Enum):RED=0GREEN=1BLUE=2matchcolor:caseColor.RED:print("I see red!")caseColor.GREEN:print("Grass is green")caseColor.BLUE:print("I'm feeling the blues :(")
This document is placed in the public domain or under theCC0-1.0-Universal license, whichever is more permissive.
Source:https://github.com/python/peps/blob/main/peps/pep-0636.rst
Last modified:2025-02-01 08:59:27 GMT