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.

Example of executing Python interactively

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 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()

Keep Learning with me