Python, for all its power and popularity, has long lacked a form of flow control found in other languages—a way to take a value and match it elegantly against one of a number of possible conditions. In C and C++, it’s the switch/case
construction; in Rust, it’s called “pattern matching.”
The traditional ways to do this in Python aren’t elegant. One is to write an if/elif/else
chain of expressions. The other is to store values to match as keys in a dictionary, then use the values to take an action—e.g., store a function as a value and use the key or some other variable as input. In many cases this works well, but can be cumbersome to construct and maintain.
After many proposals to add a switch/case
-like syntax to Python failed, a recent proposal by Python language creator Guido van Rossum and a number of other contributors has been accepted for Python 3.10: structural pattern matching. Structural pattern matching not only makes it possible to perform simple switch/case
style matches, but also supports a broader range of use cases.
Introducing Python structural pattern matching
Structural pattern matching introduces the match/case
statement and the pattern syntax to Python. The match/case
statement follows the same basic outline as switch/case
. It takes an object, tests the object against one or more match patterns, and takes an action if it finds a match.
match command: case "quit": quit() case "reset": reset() case unknown_command: print (f"Unknown command '{unknown_command}'")
Each case
statement is followed by a pattern to match against.
In the above example we’re using simple strings as our match targets, but more complex matches are possible. In fact, the chief use case for structural pattern matching is to match patterns of types, rather than patterns of values.
Python performs matches by going through the list of cases from top to bottom. On the first match, Python executes the statements in the corresponding case
block, then skips to the end of the match
block and continues with the rest of the program. There is no “fall-through” between cases, but it’s possible to design your logic to handle multiple possible cases in a single case
block. (More on this later.)
It’s also possible to capture all or part of a match and re-use it. In the case unknown_command
in our example above, where no match has been made, the value is “captured” in the variable unknown_command
so we can re-use it.
Matching against variables with Python structural pattern matching
An important note is worth bringing up here. If you list variable names in a case
statement, that doesn’t mean a match should be made against the contents of the named variable. Variables in a case
are used to capture the value that is being matched.
If you want to match against the contents of a variable, that variable must be expressed as a dotted name, like an enum. Here’s an example:
from enum import Enum class Command(Enum): QUIT = 0 RESET = 1 match command: case Command.QUIT: quit() case Command.RESET: reset()
One doesn’t have to use an enum; any dotted-property name will do. That said, enums tend to be the most familiar and idiomatic way to do this in Python.
You cannot match against variable contents through indexing. For instance, case commands[0]:
would be rejected as a syntax error.
Matching against multiple elements with Python structural pattern matching
The key to working most effectively with pattern matching is not just to use it as a substitute for a dictionary lookup or an if/else chain. It’s to describe the structure of what you want to match. This way, you can perform matches based on the number of elements you’re matching against, or their combination.
Here’s a slightly more complex example. Here, the user types in a command, optionally followed by a filename.
command = input("Command:") match command.split(): case ["quit"]: quit() case ["load", filename]: load_from(filename) case ["save", filename]: save_to(filename) case _: print (f"Command '{command}' not understood")
Let’s examine these cases in order:
case ["quit"]:
tests if what we’rematch
ing against is a list with just the item"quit"
, derived from splitting the input.case ["load", filename]:
tests if the first split element is the string"load"
, and if there’s a second string that follows. If so, we store the second string in the variablefilename
and use it for further work. Same forcase ["save", filename]:
.case _:
is a wildcard match. It matches if no other match has been made up to this point. Note that the underscore variable,_
, doesn’t actually bind to anything; the name_
is used as a signal to thematch
command that the case in question is a wildcard. (That’s why we refer to the variablecommand
in the body of thecase
block; nothing has been captured.)
Patterns in Python structural pattern matching
Patterns can be simple values, or they can contain more complex matching logic. Some examples:
case "a"
: Match against the single value"a"
.case ["a","b"]
: Match against the collection["a","b"]
.case ["a", value1]
: Match against a collection with two values, and place the second value in the capture variablevalue1
.case ["a", *values]
: Match against a collection with at least one value. The other values, if any, are stored invalues
. Note that you can include only one starred item per collection (as it would be with star arguments in a Python function).case ("a"|"b"|"c")
: Theor
operator (|
) can be used to allow multiple cases to be handled in a singlecase
block. Here, we match against either"a"
,"b"
, or"c"
.case ("a"|"b"|"c") as letter
: Same as above, except we now place the matched item into the variableletter
.case ["a", value] if <expression>
: Matches the capture only ifexpression
is true. Capture variables can be used in the expression. For instance, if we usedif value in valid_values
, thecase
would only be valid if the captured valuevalue
was in fact in the collectionvalid_values
.case ["z", _]
: Any collection of items that begins with"z"
will match.
Matching against objects with Python structural pattern matching
The most advanced feature of Python’s structural pattern matching system is the ability to match against objects with specific properties. Consider an application where we’re working with an object named media_object
, which we want to convert into a .jpg
file and return from the function.
match media_object: case Image(codec="jpg"): # Return as-is return media_object case Image(codec="png") | Image(codec="gif"): return render_as(media_object, "jpg") case Video(): raise ValueError("Can't extract frames from video yet") case other_type: raise Exception(f"Media object {media_object} of type {codec} can't be handled yet")
In each case above, we’re looking for a specific kind of object, sometimes with specific attributes. The first case matches against an Image
object with the codec
attribute set to "jpg"
. The second case matches if type
is "png"
or "gif"
. The third case matches any object of type Video
, no matter its attributes. And the final case is our catch-all if everything else fails, although we use an actual name for the match to capture it instead of _
.
You can also perform captures with object matches:
match media_object: case Image(codec=media_type): print (f"Image of type {media_type}")
Using Python structural pattern matching effectively
The key with Python structural pattern matching is to write matches that cover the structural cases you’re trying to match against. Simple tests against constants are fine, but if that’s all you’re doing, then a simple dictionary lookup might be a better option. The real value of structural pattern matching is being able to make matches against a pattern of objects, not just one particular object or even a selection of them.
Another important thing to bear in mind is the order of the matches. Which matches you test for first will have an impact on the efficiency and accuracy of your matching overall. Most folks who have built lengthy if/elif/else
chains will realize this, but pattern matching requires you to think about order even more carefully, due to the potential complexity. Place your most specific matches first, and the most general matches last.
Finally, if you have a problem that could be solved with a simple if/elif/else
chain, or a dictionary lookup — use that instead! Pattern matching is powerful, but not universal. Use it when it makes the most sense for a problem.