Folks migrating from another programming language into Python often look for a familiar switch-case statement, which Python does not have.
Python does have a match-case statement, but it's not the same thing as switch-case.
And usually, when you're looking for switch-case, I wouldn't recommend using match-case.
match-casePython's match statement is for structural pattern-matching, which sounds complicated because it is a bit complicated.
The match statement has a different way of parsing expressions within its case statements that's kind of an extension of the way that Python parses code in general.
And again, that sounds complex because it is.
I'll cover the full power of match-case another time, but let's quickly look at a few examples that demonstrate the power of match-case.
Python's match statement can be used to match an iterable based on the number of items it contains (one, two, or three items, for example):
>>> point = (3, 4)
>>> match point:
... case (x,):
... print(f"1D point at {x}")
... case (x, y):
... print(f"2D point at ({x}, {y})")
... case (x, y, z):
... print(f"3D point at ({x}, {y}, {z})")
...
2D point at (3, 4)
The match statement can also match an iterable based on its actual values.
Here, we're matching the first value as being move, and the second one as being either up, down, left, or right.
Or we're matching the first value as being turn:
>>> command = ["move", "up", 10]
>>> match command:
... case ["move", "up" | "down" | "left" | "right" as direction, distance]:
... print(f"Moving {direction} {distance} units")
... case ["turn", direction]:
... print(f"Turning {direction}")
...
Moving up 10 units
The match statement can also match a dictionary based on the keys that it contains.
Here, we're matching a dictionary with the key of type, the key of error, or something else:
>>> config = {"type": "rectangle", "width": 3, "height": 4}
>>> match config:
... case {"type": type}:
... print(f"Object is {type}")
... case {"error": message}:
... print(f"Error: {message}")
... case _:
... print(f"Unknown response")
...
Object is rectangle
We can also match a dictionary that has specific keys and specific values.
Here, we're looking for a type of circle, for example:
>>> config = {"type": "rectangle", "width": 3, "height": 4}
>>> match config:
... case {"type": "circle", "radius": r}:
... print(f"Circle with radius {r}")
... case {"type": "rectangle", "width": w, "height": h}:
... print(f"Rectangle {w}x{h}")
...
Rectangle 3x4
The match statement can also match an object based on its type:
>>> value = 4
>>> match value:
... case int():
... print(f"Integer: {value}")
... case str():
... print(f"String: {value}")
... case list():
... print(f"List with {len(value)} items")
... case _:
... print("Unknown type")
...
Integer: 4
It can also match objects based on the presence of certain attributes, or specific attributes having specific values:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def describe_point(point):
match point:
case Point(x=0, y=0):
return "Point at the origin"
case Point(x=0, y=y):
return f"On Y-axis at {y}"
case Point(x=x, y=0):
return f"On X-axis at {x}"
case Point(x=x, y=y):
return f"Point at ({x}, {y})"
For example, this match-case statement will match the Point(0, 0) as at the origin, but Point(0, 1) as on the y-axis at position 1:
>>> describe_point(Point(0, 0))
'Point at the origin'
>>> describe_point(Point(0, 1))
'On Y-axis at 1'
The various forms of structural pattern-matching can also be deeply nested.
Here, we're using a match-case statement with Python's ast module to parse Python code:
>>> find_sorted_assignments("x = sorted(y)")
Found: x assigned to sorted(y, ...)
This match-case statement is matching an Assign node, which has a Call node within it.
That Call node is calling the [sorted][] function, while grabbing the name of the first argument passed to sorted, and grabbing the name of the variable that we're assigning to:
import ast
def find_sorted_assignments(python_code):
for node in ast.parse(python_code).body:
match node:
case ast.Assign(
targets=[target],
value=ast.Call(
func=ast.Name(id="sorted"),
args=[iterable],
),
):
target = ast.unparse(target)
iterable = ast.unparse(iterable)
print(f"Found: {target} assigned to sorted({iterable}, ...)")
That's a pretty complex match-case statement!
The match statement was designed to handle some pretty sophisticated pattern-matching.
It really shines when implementing parsers like this.
match is not designed to be an equivalent to switch.
But why should you not use match as an equivalent to the switch statement?
if-elif instead of switch-caseIn Python, if-elif blocks are pretty readable.
Compare this match-case block...
def get_day_type(day):
match day.lower():
case "monday" | "tuesday" | "wednesday" | "thursday" | "friday":
return "weekday"
case "saturday" | "sunday":
return "weekend"
case _:
return "invalid"
... to this if-elif block:
def get_day_type(day):
day_lower = day.lower()
if day_lower in ("monday", "tuesday", "wednesday", "thursday", "friday"):
return "weekday"
elif day_lower in ("saturday", "sunday"):
return "weekend"
else:
return "invalid"
I don't find the match block all that much easier to understand than the if block.
Also, unlike match-case, the if statement doesn't require an extra level of indentation.
And everyone probably understands the if-elif code.
But not everyone necessarily understands the match-case code.
It is nice to avoid long if-elif statements.
But replacing them with match-case doesn't help much.
switch-caseSometimes you can replace long if statements with dictionaries.
Here's some code that converts HTTP status codes to their descriptions:
def get_status_message(code):
if code == 200:
return "OK"
elif code == 201:
return "Created"
elif code == 400:
return "Bad Request"
elif code == 401:
return "Unauthorized"
elif code == 404:
return "Not Found"
elif code == 500:
return "Internal Server Error"
else:
return "Unknown Status"
Note that we're essentially converting our input values to our output values. Each valid input value maps to a possible output value.
Python has a data structure for mapping data just like this... it's called a dictionary.
Here's the same code that uses a dictionary instead of a long if-elif chain:
STATUS_MESSAGES = {
200: "OK",
201: "Created",
400: "Bad Request",
401: "Unauthorized",
404: "Not Found",
500: "Internal Server Error"
}
def get_status_message(code):
return STATUS_MESSAGES.get(
code,
"Unknown Status",
)
I find this much more readable. I like that we've moved our key-value mappings into a data structure.
The code from before looked like a lot of logic, but it was really data disguised as logic.
The dictionary makes it clearer that this bit of code is data-heavy, but not actually very logic-heavy.
Most Python programmers are not very familiar with match-case.
It's not used very often.
And when it is used, it's typically used for pattern-matching, not for matching a single value.
The next time you find yourself tempted to use match-case, ask yourself whether a dictionary might be better instead.
Need to fill-in gaps in your Python skills?
Sign up for my Python newsletter where I share one of my favorite Python tips every week.
Need to fill-in gaps in your Python skills? I send weekly emails designed to do just that.
Sign in to your Python Morsels account to track your progress.
Don't have an account yet? Sign up here.