Execute Program

Python for Programmers: Pattern Matching

Welcome to the Pattern Matching lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • Python has a match / case construct similar to switch / case in JavaScript, C, and many other languages. When we match some_value, Python runs the case that corresponds to the value.

  • >
    def speak(animal):
    match animal:
    case "dog":
    return "woof!"
    case "cat":
    return "meow"
    case "cow":
    return "moo"
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    speak('dog')
    Result:
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    speak('cat')
    Result:
    'meow'Pass Icon
  • If no case matches, the match returns None.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    speak("bear")
    Result:
    NonePass Icon
  • match can also match other types like numbers, booleans, and lists.

  • >
    def identify(data):
    match data:
    case [1, 2]:
    return "list"
    case 23:
    return "number"
    case False:
    return "boolean"
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    identify([1, 2])
    Result:
    'list'Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    identify(False)
    Result:
    'boolean'Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    identify(True)
    Result:
    NonePass Icon
  • At first, match / case seems identical to the switch / case syntax in other languages. However, there's a lot more to Python's match! For example, match can unpack elements from a list, tuple, or another iterable.

  • (Remember, variables named _ tell other programmers that we won't use that value. When unpacking, *_ means "collect the rest of the elements into a variable _, which we don't plan to use".)

  • >
    some_list = [6, 3, 2, 1, 4]
    _, second, *_ = some_list

    second
    Result:
    3Pass Icon
  • We can use that same syntax to unpack the second element inside of a match.

  • >
    def second_element(my_list):
    match my_list:
    case [_, second, *_]:
    return second
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    second_element([6, 3, 2, 1, 4])
    Result:
    3Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    second_element(['x', 'y', 'z'])
    Result:
    'y'Pass Icon
  • When there's no second element, the case pattern doesn't match and we get None.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    second_element([6])
    Result:
    NonePass Icon
  • Let's add a second case to handle lists with no second element. This is like writing an else: clause on a conditional: we want it to happen whenever our main case doesn't match. By convention, Python programmers write that as case _:. That's a pattern that matches any value, which we then store in the _ variable.

  • >
    def second_element(my_list):
    match my_list:
    case [_, second, *_]:
    return second
    case _:
    return "nothing"

    second_element([6])
    Result:
  • We can build on this to match only lists and tuples with a certain constant value in a specific place.

  • (In the next example, [*_] means "any list with any elements." Like before, we're storing a value in the _ variable, which we never use again.)

  • >
    def amir_is_first(my_list):
    match my_list:
    case ["Amir", *_]:
    return True
    case [*_]:
    return False
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    amir_is_first(["Amir", "Betty", "Cindy"])
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    amir_is_first(["Betty", "Cindy", "Amir"])
    Result:
    FalsePass Icon
  • As usual, we get None when none of the cases match.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    amir_is_first("not a list")
    Result:
    NonePass Icon
  • Python's match is powerful and works with many data types. The next example decides which actions a user can take based on their "role" attribute. There are three roles with increasing access: "user", "manager", and "admin".

  • In the next example, we match dicts like case {"role": "admin"}. That doesn't mean "only match dicts with only that key and that value." Instead, it means "match any dict where "role" is "admin", even if there are other keys present."

  • >
    def allowed_actions(user):
    match user:
    case {"role": "admin"}:
    return ("log-in", "view-other-users", "delete-users")
    case {"role": "manager"}:
    return ("log-in", "view-other-users")
    case _:
    return ("log-in",)

    def action_is_allowed(user, action):
    return action in allowed_actions(user)

    amirUser = {"name": "Amir", "role": "user"}
    bettyManager = {"name": "Betty", "role": "manager"}
    cindyAdmin = {"name": "Cindy", "role": "admin"}
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    action_is_allowed(amirUser, "log-in")
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    action_is_allowed(amirUser, "view-other-users")
    Result:
    FalsePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    action_is_allowed(bettyManager, "view-other-users")
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    action_is_allowed(bettyManager, "delete-users")
    Result:
    FalsePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    action_is_allowed(cindyAdmin, "delete-users")
    Result:
    TruePass Icon
  • A case can also match on the value's type (its class) and its attributes. It can even extract specific attributes to store in variables.

  • The next example does both of those things. We have two cases that select cats where vaccinated is True or False. Both cases extract the cats' name attributes.

  • >
    class Cat:
    def __init__(self, name, vaccinated):
    self.name = name
    self.vaccinated = vaccinated

    def vaccination_status(cat):
    match cat:
    case Cat(name=name, vaccinated=True):
    return f"{name} is vaccinated"
    case Cat(name=name, vaccinated=False):
    return f"{name} is not vaccinated"
    case _:
    return "That's not a cat!"
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    keanu = Cat("Keanu", True)
    vaccination_status(keanu)
    Result:
    'Keanu is vaccinated'Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    ms_fluff = Cat("Ms. Fluff", False)
    vaccination_status(ms_fluff)
    Result:
    'Ms. Fluff is not vaccinated'Pass Icon
  • Since we're matching case Cat(...), non-cat values won't match.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    cat_dict = {
    "name": "Keanu",
    "vaccinated": True
    }
    vaccination_status(cat_dict)
    Result:
    "That's not a cat!"Pass Icon
  • cases can match multiple conditions at once. We do that with |, which often means "or" in programming languages.

  • >
    def is_list_or_tuple(value):
    match value:
    case [*_] | (*_, ):
    return True
    case _:
    return False
  • That case expression contains a lot of punctuation, so let's break it down:

    • [*_] matches any list with any number of elements.
    • (*_, ) matches any tuple with any number of elements. Remember, the , makes it a tuple!
    • | means "match when either one of these two patterns matches".
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    is_list_or_tuple([1, 2, 3])
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    is_list_or_tuple(())
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    is_list_or_tuple({})
    Result:
    FalsePass Icon
  • As with many of Python's features, it's important not to get carried away. It's easy to accidentally write a complex match when we could use a simpler expression, like a simple if / else.

  • Depending on your preferences, it might be fun to think about match cases like the [*_] | (*_,) case above. But it would be better to write a simple, boring expression like this one:

  • >
    def is_list_or_tuple(value):
    return isinstance(value, (list, tuple))
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    is_list_or_tuple([1, 2, 3])
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    is_list_or_tuple(())
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    is_list_or_tuple({})
    Result:
    FalsePass Icon
  • Although it can be overused, match is great for checking complex conditions, and for unpacking specific parts of the matched value.