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/caseconstruct similar toswitch/casein JavaScript, C, and many other languages. When wematch some_value, Python runs thecasethat 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'
If no case matches, the
matchreturnsNone.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
speak("bear")Result:
None
matchcan 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'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
identify(False)Result:
'boolean'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
identify(True)Result:
None
At first,
match/caseseems identical to theswitch/casesyntax in other languages. However, there's a lot more to Python'smatch! For example,matchcan 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_listsecondResult:
3
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:
3
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
second_element(['x', 'y', 'z'])Result:
'y'
When there's no second element, the
casepattern doesn't match and we getNone.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
second_element([6])Result:
None
Let's add a second
caseto handle lists with no second element. This is like writing anelse:clause on a conditional: we want it to happen whenever our maincasedoesn't match. By convention, Python programmers write that ascase _:. 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 secondcase _: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 Truecase [*_]:return False- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
amir_is_first(["Amir", "Betty", "Cindy"])Result:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
amir_is_first(["Betty", "Cindy", "Amir"])Result:
False
As usual, we get
Nonewhen none of the cases match.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
amir_is_first("not a list")Result:
None
Python's
matchis 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:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
action_is_allowed(amirUser, "view-other-users")Result:
False
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
action_is_allowed(bettyManager, "view-other-users")Result:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
action_is_allowed(bettyManager, "delete-users")Result:
False
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
action_is_allowed(cindyAdmin, "delete-users")Result:
True
A
casecan 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 wherevaccinatedisTrueorFalse. Bothcases extract the cats'nameattributes.>
class Cat:def __init__(self, name, vaccinated):self.name = nameself.vaccinated = vaccinateddef 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'
- 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'
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!"
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 Truecase _:return FalseThat
caseexpression 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:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
is_list_or_tuple(())Result:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
is_list_or_tuple({})Result:
False
As with many of Python's features, it's important not to get carried away. It's easy to accidentally write a complex
matchwhen we could use a simpler expression, like a simpleif/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:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
is_list_or_tuple(())Result:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
is_list_or_tuple({})Result:
False
Although it can be overused,
matchis great for checking complex conditions, and for unpacking specific parts of the matched value.