Python Match Case is more powerful than you think ππΉοΈ by https://fediverse.org/guilatrova ΒΆ
Introduction ΒΆ
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!
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 equals End
-
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()