The Match-Case In Python 3.10 Is Not That Simple | by Christopher Tao | Towards Data Science

In my previous article, I have introduced all the major new features in Python 3.10, which has just been released 2 weeks ago. One of the most important features is the Match-Case syntax.
Some still argue that Python doesn’t need the “switch-case” syntax. Even Guido himself did not support adding this syntax in Python. However, why it is still released in this new version? In my opinion, the reason can be found in the name. It is called “match case” rather than “switch case” like most programming languages do.
In this article, I’ll use 7 examples to demonstrate how flexible and “pythonic” the new syntax is. Hope it will help you to understand it a bit easier.
If you are interested in the other features in Python 3.10, please refer to my previous post.
towardsdatascience.com
Before any fancy stuff, let’s start with the basic use case of this new syntax. Suppose we are writing some code to translate the HTTP status code into error messages, we can use the match-case syntax as follows.
def http_status(status):    match status:        case 400:            return "Bad request"        case 401:            return "Unauthorized"        case 403:            return "Forbidden"        case 404:            return "Not found"
Indeed, for this particular example, the match-case didn’t bring any benefits than the if-else syntax, as follow.
def http_error(status):    if status == 400:        return "Bad request"    elif status == 401:        return "Unauthorized"    elif status == 403:        return "Forbidden"    elif status == 404:        return "Not found"    else:        return "Unknown status code"
If it can only do such a thing, I wouldn’t think it will be added. Let’s keep going with more examples to see what flexibility it can have.
It’s meant to be similar to the switch-case syntax in most other languages, so it must have the “default case”. When no defined case can be matched, the code in “default case” will be executed.
Python achieves this requirement in its style. It makes use of the underscore “_” that stands for an anonymous variable. The rationale is that an anonymous variable can “match” anything.
Let’s see the example below.
def http_status(status):    match status:        case 400:            return "Bad request"        case 401:            return "Unauthorized"        case 403:            return "Forbidden"        case 404:            return "Not found"        case _:            return "Other error"
In the code above, we have added the default case and display “Other error”.
What if sometimes we have multiple cases that should be combined? In other words, these are different cases, but the way we handle them should be the same.
In Python, we can use a pipe “|” to combine these cases into a single case. It is also considered an “OR” relationship.
def http_status(status):    match status:        case 400:            return "Bad request"        case 401 | 403:            return "Authentication error"        case 404:            return "Not found"        case _:            return "Other error"
In the code above, we consider both the 401 and 403 errors as authentication issues, so we can combine them in the match-case.
The more interesting stuff is coming now. Suppose we are writing an alarm logic using the match-case syntax. It accepts a list as the argument. The first element is the time of the day. Let’s keep it simple so we have “morning”, “afternoon” and “evening”. The second element will be the action that we need the alarm to remind us to do.
However, we may want the alarm to remind us to do multiple things at the same time. Therefore, we want to pass more than one action in the list so that the alarm can remind us to do all of them one by one.
Here is the code to achieve such a requirement.
def alarm(item):    match item:        case [time, action]:            print(f'Good {time}! It is time to {action}!')        case [time, *actions]:            print('Good morning!')            for action in actions:                print(f'It is time to {action}!')
In this example, we only have two cases. The first case doesn’t need to be explained too much as it just tries to match the single-action case. In the second case, we put an asterisk in front of the variable “actions”, so it can match one or more actions in the list!
Let’s give it a try.
alarm(['afternoon', 'work'])alarm(('morning', 'have breakfast', 'brush teeth', 'work'))
Sometimes, we may want to have patterns in a pattern. Specifically, we want the match-case to triage the flow into a particular “case”. However, inside that pattern, we want to add some more restrictions.
Remember we have defined that the “time” of the day must be either “morning”, “afternoon” or “evening”? Let’s add this restriction to the match-case code. If the “time” doesn’t match one of these 3, tell the user the time is invalid.
def alarm(item):    match item:        case [('morning' | 'afternoon' | 'evening'), action]:            print(f'Good (?)! It is time to {action}!')        case _:            print('The time is invalid.')
In the above code, it shows that we can use a pair of parentheses to enclose the “patterns” that we want to match, and use pipes to separate these patterns.
If it doesn’t match one of the given 3 “times”, the default case will be executed.
Why there is a question mark there? I intentionally added it because I want to emphasise the solution. Generally, it won’t be easy to match one of the sub-patterns and then reference which pattern it matched exactly. However, we can have this “reference” in Python.
Let’s add an “as” keyword followed by a variable, and this variable will be the reference.
def alarm(item):    match item:        case [('morning' | 'afternoon' | 'evening') as time, action]:            print(f'Good {time}! It is time to {action}!')        case _:            print('The time is invalid.')
We need to let this alarm more “smart”. When it is evening, let’s display some messages to appreciate the user for finishing the day for work.
def alarm(item):    match item:        case ['evening', action]:            print(f'You almost finished the day! Now {action}!')        case [time, action]:            print(f'Good {time}! It is time to {action}!')        case _:            print('The time is invalid.')
Therefore, we have added one more case in the above code to match “evening” only.
Hmmm... the second one seems a bit frustrated. Let’s make this alarm even smarter to encourage the user to be “work-life balance”. Therefore, when the user wants to work or study in the evening, the alarm recommends the user to have some rest. (I hope I can have such an alarm :D)
def alarm(item):    match item:        case ['evening', action] if action not in ['work', 'study']:            print(f'You almost finished the day! Now {action}!')        case ['evening', _]:            print('Come on, you deserve some rest!')        case [time, action]:            print(f'Good {time}! It is time to {action}!')        case _:            print('The time is invalid.')
To achieve this, we have added another case on the top. It comes with a condition. That is, when the action is either “work” or “study”, display some different messages.
So far, we have explored a lot. I guess you may have such a feeling, it is a “match-case” rather than a “switch-case”, because the “pattern” is always the key.
Let’s have an even more complex example — a class instance. Yes, let’s use the match-case to match an object.
I’ll just make up a simple use case. A class called “Direction” is created for holding the direction on the horizontal (east or west) and vertical (north or south) axis.
class Direction:    def __init__(self, horizontal=None, vertical=None):        self.horizontal = horizontal        self.vertical = vertical
Now, we want to use the match-case syntax to match an instance from this class and display a message based on the attribute.
def direction(loc):    match loc:        case Direction(horizontal='east', vertical='north'):            print('You towards northeast')        case Direction(horizontal='east', vertical='south'):            print('You towards southeast')        case Direction(horizontal='west', vertical='north'):            print('You towards northwest')        case Direction(horizontal='west', vertical='south'):            print('You towards southwest')        case Direction(horizontal=None):            print(f'You towards {loc.vertical}')        case Direction(vertical=None):            print(f'You towards {loc.horizontal}')        case _:            print('Invalid Direction')
Then, let’s create some objects from the class for testing.
d1 = Direction('east', 'south')d2 = Direction(vertical='north')d3 = Direction('centre', 'centre')
The results show that the match-case can easily match these objects based on their attributes!
direction(d1)direction(d2)direction(d3)
In this article, I have introduced the new syntax “match-case” that is introduced in Python 3.10, and how flexible and powerful it is. I believe nailing this new syntax will help us to write better code in terms of readability.
medium.com
If you feel my articles are helpful, please consider joining Medium Membership to support me and thousands of other writers! (Click the link above)