Juha-Matti Santala
Community Builder. Dreamer. Adventurer.

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 variable obj
  • An array with two values, first one being "go" and second being anything, bound into variable direction

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