Python Match Case is more powerful than you think ππΉοΈ
Python 3.10 brought the match case
syntax which is similar to the switch case
from other languages.
It's just similar though. Python's match case is WAY MORE POWERFUL than the switch case because it's a Structural Pattern Matching.
You don't know what I mean? I'm going to show you what it can do with examples!
Note that if you're reading this article in AMP mode or from mobile you won't be able to run Python code from your browser, but you can still see the code samples.
Match Case is similar to a Switch Case
It's still possible to use match case
as a common switch case
:
from http import HTTPStatus
import random
http_status = random.choice(
[
HTTPStatus.OK,
HTTPStatus.BAD_REQUEST,
HTTPStatus.INTERNAL_SERVER_ERROR,
]
)
# π Simplest example, can be easily replaced by a dictionary
match http_status:
case HTTPStatus.OK: # π "case" + "value" syntax
print("Everything is good!")
case HTTPStatus.BAD_REQUEST:
print("You did something wrong!")
case HTTPStatus.INTERNAL_SERVER_ERROR:
print("Oops... Is the server down!?.")
case _: # π Default syntax
print("Invalid or unknown status.")
Boring, right? It can be easily replaced by a dictionary with fewer lines, see:
from http import HTTPStatus
import random
http_status = random.choice(
[
HTTPStatus.OK,
HTTPStatus.BAD_REQUEST,
HTTPStatus.INTERNAL_SERVER_ERROR,
]
)
dictmap = {
HTTPStatus.OK: "Everything is good!",
HTTPStatus.BAD_REQUEST: "You did something wrong!",
HTTPStatus.INTERNAL_SERVER_ERROR: "Oops... Is the server down!?.",
}
message = dictmap.get(http_status, "Invalid or unknown status.")
print(message)
Match Case matching many different values
As I mentioned initially, the match case goes beyond a regular switch case.
Let's match specific status codes with the or
statement by using |
:
from http import HTTPStatus
import random
http_status = random.choice(list(HTTPStatus))
match http_status:
case 200 | 201 | 204 as status:
# π Using "as status" extracts its value
print(f"Everything is good! {status = }") # π Now status can be used inside handler
case 400 | 404 as status:
print(f"You did something wrong! {status = }")
case 500 as status:
print(f"Oops... Is the server down!? {status = }")
case _ as status:
print(f"No clue what to do with {status = }!")
Note we used as status
to extract the value into a variable that can be used inside the handler.
Match Case with conditionals (guards)
Not exciting yet? Ok, let's improve it a little.
You can see we are missing many status codes in the previous example.
What if we want to match ranges as:
- <200,
- 200-399,
- 400-499, and
- >=500?
We can use guards for that:
from http import HTTPStatus
import random
http_status = random.choice(list(HTTPStatus))
match http_status:
# πββοΈ Note we don't match a specific value as we use "_" (underscore)
# πβ
Match any value, as long as status is between 200-399
case _ as status if status >= HTTPStatus.OK and status < HTTPStatus.BAD_REQUEST:
print(f"β
Everything is good! {status = }")
# ππ€ We took 'status' by using the 'as status' syntax
# πβ Match any value, as long as status is between 400-499
case _ as status if status >= HTTPStatus.BAD_REQUEST and status < HTTPStatus.INTERNAL_SERVER_ERROR:
print(f"β You did something wrong! {status = }")
# ππ£ Match any value, as long as status is >=500
case _ as status if status >= HTTPStatus.INTERNAL_SERVER_ERROR:
print(f"π£ Oops... Is the server down!? {status = }.")
# πβ Match any value that we didn't catch before (<200)
case _ as status:
print(f"β No clue what to do with {status = }!")
π Note we didn't use any specific value inside our case statements.
We used _
(underscore) to match all because we wanted to check ranges instead of specific values.
We call "guards" when we validate the matched pattern using an if
as we did above.
Match Case lists value, position, and length
You can match lists based on values at a specific position and even length!
See some examples below where we match:
- Any list with 3 items by using and extracting these items as vars
- Any list with more than 3 items by using
*_
- Any list starting with a specific value + possible combinations
- Any list starting with a specific value
baskets = [
["apple", "pear", "banana"], # π π π
["chocolate", "strawberry"], # π« π
["chocolate", "banana"], # π« π
["chocolate", "pineapple"], # π« π
["apple", "pear", "banana", "chocolate"], # π π π π«
]
def resolve_basket(basket: list):
match basket:
# π Matches any 3 items
case [i1, i2, i3]: # π These are extracted as vars and used here π
print(f"Wow, your basket is full with: '{i1}', '{i2}' and '{i3}'")
# π Matches >= 4 items
case [_, _, _, *_] as basket_items:
print(f"Wow, your basket has so many items: {len(basket_items)}")
# π 2 items. First should be π«, second should be π or π
case ["chocolate", "strawberry" | "banana"]:
print("This is a superb combination. π« + π|π")
# π 2 items. First should be π«, second should be π
case ["chocolate", "pineapple"]:
print("Eww, really? π« + π = ?")
# π Any amount of items starting with π«
case ["chocolate", *_]:
print("I don't know what you plan but it looks delicious. π«")
# π If nothing matched before
case _:
print("Don't be cheap, buy something else")
for basket in baskets:
print(f"π₯ {basket}")
resolve_basket(basket)
print()
Match Case dicts
We can do a lot with dicts!
Let's see many examples with dicts holding str
keys and either int
or str
as their values.
We can match existing keys, value types, and dict length.
mappings: list[dict[str, str | int]] = [
{"name": "Gui Latrova", "twitter_handle": "@guilatrova"},
{"name": "John Doe"},
{"name": "JOHN DOE"},
{"name": 123456},
{"full_name": "Peter Parker"},
{"full_name": "Peter Parker", "age": 16}
]
def resolve_mapping(mapping: dict[str|int]):
match mapping:
# π Matches any
# (1) "name" AND any (2) "twitter_handle"
case {"name": name, "twitter_handle": handle}:
print(f"π Make sure to follow {name} at {handle} to keep learning") # π This is good advice
# π Matches any
# (1) "name" (2) if val is str and (3) it's all UPPER CASED
case {"name": str() as name} if name == name.upper():
print(f"π₯ Hey, there's no need to shout, {name}!")
# π Matches any
# (1) "name" (2) if val is str. It will fall here whenever the above π doesn't match
case {"name": str() as name}:
print(f"π Hi {name}!")
# π Matches any
# (1) "name" (2) if val is int.
case {"name": int()}:
print("π€ Are you a robot or what? How can I say your name? ")
# π Matches any
# (1) "full_name" (2) and NOTHING else
case {"full_name": full_name, **remainder} if not remainder:
print(f"Thanks mr/ms {full_name}!")
# π Matches any
# (1) "full_name" (2) and ANYTHING else
case {"full_name": full_name, **remainder}:
print(f"Just your full name is fine! No need to share {list(remainder.keys())}")
for mapping in mappings:
print(f"π₯ {mapping}")
resolve_mapping(mapping)
print()
Match Case classes instances and props
The first time I saw:
class Example:
...
var = Example()
match var:
case Example(): # π This syntax is a bit weird
...
I thought we could be instantiating the class π which is wrong.
This syntax means: "Instance of type Example
with any props."
Above you probably saw we doing that for int()
and str()
. The logic is the same.
Check a few examples:
- Matching a class instance with the property
name
equalsEnd
- Matching any instance based on the type
- Matching instances with specific properties set to
0
- Extracting class properties to be used inside the handler
from dataclasses import dataclass
@dataclass
class Move:
x: int # horizontal
y: int # vertical
@dataclass
class Action:
name: str
@dataclass
class UnknownStep:
random_value = "Darth Vader riding a monocycle"
steps = [
Move(1, 0),
Move(2, 5),
Move(0, 5),
Action("Work"),
Move(0, 0),
Action("Rest"),
Move(0, 0),
UnknownStep(),
Action("End"),
]
def resolve_step(step):
match step:
# π Match any action that has name = "End"
case Action(name="End"): # π Note we're not instantiating anything
print("π Flow finished")
# π Match any Action type
case Action():
print("π Good to see you're doing something")
# π Match any Move with x,y == 0,0
case Move(0, 0):
print("π You're not really moving, stop pretending")
# π Match any Move with y = 0
case Move(x, 0):
print(f"β‘οΈ You're moving horizontally to {x}")
# π Match any Move with x = 0
case Move(0, y):
print(f"π You're moving vertically to {y}")
# π Match any Move type
case Move(x, y):
print(f"πΊοΈ You're moving to ({x}, {y})")
# π When nothing matches
case _:
print(f"β I've got not idea what you're doing")
for step in steps:
print(f"π₯ {step}")
resolve_step(step)
print()