Pattern matching is coming to Python
I was really excited when last week, a set of PEPs (Python Enhancement Proposals) that introduced pattern matching to Python language were accepted. Pattern matching is one of those things that ever since I learned to use them in other languages, I've really wanted to have in Python.
The three accepted proposals are: PEP 634 that introduces the Structural Pattern Matching's specification, PEP 635 that introduces the motivation and rationale and PEP 636 that introduces a tutorial to how it works.
Pattern matching is scheduled to arrive in Python 3.10, in the fall of 2021.
What is pattern matching?
Pattern matching is a language structure that allows the developer to essentially do two things: to branch out based on the structure of the variable and to bind values to variables for further use. It provides a nicer interface to things that could be done with if/else clauses.
The example shown in PEP 636 is quite a good example of how pattern matching can make the code much easier to read and follow:
match command.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
Here, the output of command.split()
is matched against different
cases:
- An array with a single value
"quit"
- An array with a single value
"look"
-
An array with two values, first one being
"get"
and second being anything, bound into variableobj
-
An array with two values, first one being
"go"
and second being anything, bound into variabledirection
Thanks to the structural pattern matching, we don't have to first branch out based on how many items there are in the array and then branch out based on values but we can do both at the same time and thanks to the binding of values, it's easier to write descriptive names without extra assignments.
The above example using if/else
could look something like:
commands = command.split()
if len(commands) == 1:
operation = commands[0]
if operation == "quit":
print("Goodbye!")
quit_game()
elif operation == "look":
current_room.describe()
elif len(commands) == 2:
[operation, value] = commands
if operation == "get":
character.get(value, current_room)
elif operation == "go":
current_room = current_room.neighbor(value)
I would argue that the first example is easier to read and makes it easier to understand and reason with the logic of the code.
To match a default case, this proposal introduces a wild card case
_
:
case _:
print(f"Sorry, I couldn't understand {command}!")
or
, as
and guards
In addition to matching just based on a single structure or literal case (or a combination of those), the proposal introduces a couple of additional ways to build powerful patterns.
With |
operator, you can match multiple values:
match direction:
case "left" | "right":
print("Moving horizontally")
case "up" | "down":
print("Moving vertically")
case _:
print("I don't know how to move into that direction")
And if you want to capture the value of those multiple options, you can use
as
:
match direction:
case ("left" | "right") as direction:
print(f"Moving horizontally to {direction}")
case ("up" | "down") as direction:
print(f"Moving vertically to {direction}")
case _:
print("I don't know how to move into that direction")
and finally you can add extra conditions with guards using if
:
match points:
case [x, y] if x == y:
print("x == y")
case [x, y] if x > y:
print("x > y")
case [x, y] if x < y:
print("x < y")
Mappings
Another way you'll be able to match is based on a structure and keys of a dictionary. This is very useful when receiving data in JSON form and parsing it into a dictionary in Python:
data = get_json_data()
match data:
case { "status": status, "data": messages }:
process_messages(messages)
case { "error": error, **rest }:
process_error(error)
The way this matching works, is that any extra keys will be ignored and the
first case will match any data
that has keys "status"
and "data"
and will bind only those values. The second case will
match any object that has a key "error"
and will bind all the
others into variable rest
.
And the other ways
To get the full picture of what these proposals will bring to the language, I recommend reading them all through: PEP 634, PEP 635 and PEP 636. For example, I didn't write at all about matching objects but there are good examples in the proposals for those as well.
Is this a good thing?
The decision to accept the proposal sparked a lot of discussion (unfortunately much of it very unproductive and mean) but I have to say I'm a big fan. Pattern matching helps make code often so much easier to read, as long as it doesn't try to do too much.
As I've been learning Rust recently, pattern matching has been one of those things I really really like.
Pattern matching is not the most intuitive features though and it can be hard
to grasp at first. Luckily, everything you can do with pattern matching you
can do with if/else
so beginners don't have to dive deep into
learning pattern matching as their first thing when learning programming.
Learn more
- PEP 634: The specification for the functionality
- PEP 635: The motivation and rationale
- PEP 636: A tutorial
- Structural Pattern Matching in Python: PEPs 634-636: A talk by Daniel Moisset providing great examples
- Get Started with Pattern Matching in Python, today!: A wonderful blog post on the topic by Alex Hultnér