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'")
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: 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’re
matching 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 variable
filenameand use it for further work. Same for
case ["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 the
matchcommand that the case in question is a wildcard. (That’s why we refer to the variable
commandin the body of the
caseblock; 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
case ["a","b"]: Match against the collection
case ["a", value1]: Match against a collection with two values, and place the second value in the capture variable
case ["a", *values]: Match against a collection with at least one value. The other values, if any, are stored in
values. 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"): The
|) can be used to allow multiple cases to be handled in a single
caseblock. Here, we match against either
case ("a"|"b"|"c") as letter: Same as above, except we now place the matched item into the variable
case ["a", value] if <expression>: Matches the capture only if
expressionis true. Capture variables can be used in the expression. For instance, if we used
if value in valid_values, the
casewould only be valid if the captured value
valuewas in fact in the collection
case ["z", _]: Any collection of items that begins with
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
"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.
Copyright © 2023 IDG Communications, Inc.