4.2 match-case (Python 3.10+) — Structural Pattern Matching
Python 3.10's match-case is not a simple switch statement. It is Structural Pattern Matching, which allows you to express the structure of data itself as a pattern. Defined in PEP 634, this feature brings functional-programming-style pattern matching to Python.
Basic Syntax
# Syntax: match <expression>: / case <pattern>: / body
command = "quit"
match command:
case "quit":
print("Quit program")
case "help":
print("Show help")
case "start":
print("Start")
case _: # Wildcard pattern: matches any value (default)
print(f"Unknown command: {command}")
match compares the value against patterns from top to bottom and executes only the first matching case block. Unlike if-elif, it terminates automatically when matched — no break needed.
Literal Patterns
status_code = 404
match status_code:
case 200:
message = "OK"
case 201:
message = "Created"
case 400:
message = "Bad Request"
case 401:
message = "Unauthorized"
case 403:
message = "Forbidden"
case 404:
message = "Not Found"
case 500:
message = "Internal Server Error"
case _:
message = f"Unknown status: {status_code}"
print(message) # Not Found
# String literal patterns
lang = "python"
match lang:
case "python":
print("Python — General-purpose language")
case "rust":
print("Rust — Systems programming")
case "javascript":
print("JavaScript — Web frontend")
case _:
print(f"{lang} — Other language")
Capture Patterns
# Variables declared in case capture the value
point = (3, 7)
match point:
case (0, 0):
print("Origin")
case (x, 0):
print(f"Point on X axis: x={x}")
case (0, y):
print(f"Point on Y axis: y={y}")
case (x, y):
print(f"General point: ({x}, {y})") # Output: General point: (3, 7)
# Note: capture vs literal pattern distinction
# An already-defined variable used in a pattern captures, not compares!
HTTP_OK = 200
status = 404
match status:
# case HTTP_OK: # This captures into HTTP_OK, not literal 200!
case 200:
print("Success")
case 404:
print("Not found") # This is printed
Sequence Patterns
def describe_list(items: list) -> str:
match items:
case []:
return "Empty list"
case [single]:
return f"Single element: {single}"
case [first, second]:
return f"Two elements: {first}, {second}"
case [first, *rest]:
return f"First: {first}, remaining {len(rest)}"
print(describe_list([])) # Empty list
print(describe_list([42])) # Single element: 42
print(describe_list([1, 2])) # Two elements: 1, 2
print(describe_list([1, 2, 3, 4])) # First: 1, remaining 3
def process_command(args: list[str]) -> str:
match args:
case ["go", direction]:
return f"Move {direction}"
case ["go", direction, *extra]:
return f"Move {direction} (extra args: {extra})"
case ["pick", "up", item]:
return f"Pick up {item}"
case ["drop", item, *location]:
dest = " ".join(location) if location else "current location"
return f"Drop {item} at {dest}"
case _:
return f"Unknown command: {args}"
print(process_command(["go", "north"]))
print(process_command(["go", "east", "fast"]))
print(process_command(["pick", "up", "sword"]))
print(process_command(["drop", "shield", "safe", "room"]))
Mapping Patterns
def handle_event(event: dict) -> str:
match event:
case {"type": "click", "button": "left", "x": x, "y": y}:
return f"Left click: ({x}, {y})"
case {"type": "click", "button": "right", "x": x, "y": y}:
return f"Right click: ({x}, {y})"
case {"type": "keypress", "key": key}:
return f"Key pressed: {key}"
case {"type": "scroll", "delta": delta}:
direction = "up" if delta > 0 else "down"
return f"Scroll {direction}: {abs(delta)}"
case {"type": event_type}:
return f"Unknown event type: {event_type}"
case _:
return "Invalid event format"
events = [
{"type": "click", "button": "left", "x": 100, "y": 200},
{"type": "keypress", "key": "Enter"},
{"type": "scroll", "delta": -3},
]
for ev in events:
print(handle_event(ev))
# Mapping patterns use partial matching — extra keys are allowed
data = {"action": "login", "user": "alice", "timestamp": 1234567890}
match data:
case {"action": "login", "user": username}:
print(f"Login: {username}") # Matches even with extra 'timestamp' key
Class Patterns
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Circle:
center: Point
radius: float
@dataclass
class Rectangle:
top_left: Point
bottom_right: Point
def describe_shape(shape) -> str:
match shape:
case Point(x=0, y=0):
return "Origin"
case Point(x=x, y=0):
return f"Point on X axis: x={x}"
case Point(x=x, y=y):
return f"Point: ({x}, {y})"
case Circle(center=Point(x=cx, y=cy), radius=r):
return f"Circle: center=({cx}, {cy}), radius={r}"
case Rectangle(top_left=Point(x=x1, y=y1), bottom_right=Point(x=x2, y=y2)):
width = abs(x2 - x1)
height = abs(y2 - y1)
return f"Rectangle: width={width}, height={height}"
case _:
return f"Unknown shape: {type(shape).__name__}"
shapes = [
Point(0, 0),
Point(5, 0),
Point(3, 4),
Circle(Point(1, 2), 5.0),
Rectangle(Point(0, 10), Point(8, 0)),
]
for shape in shapes:
print(describe_shape(shape))
OR Patterns
def classify_response(code: int) -> str:
match code:
case 200 | 201 | 202 | 204:
return "Success (2xx)"
case 301 | 302 | 307 | 308:
return "Redirect (3xx)"
case 400 | 401 | 403 | 404 | 422:
return "Client error (4xx)"
case 500 | 502 | 503 | 504:
return "Server error (5xx)"
case _:
return f"Other: {code}"
for code in [200, 302, 404, 500, 999]:
print(f" {code}: {classify_response(code)}")
def handle_direction(direction: str) -> str:
match direction.lower():
case "n" | "north" | "up":
return "Move north"
case "s" | "south" | "down":
return "Move south"
case "e" | "east" | "right":
return "Move east"
case "w" | "west" | "left":
return "Move west"
case other:
return f"Unknown direction: {other}"
Guards: case x if condition
def classify_number(n: int | float) -> str:
match n:
case 0:
return "Zero"
case n if n < 0:
return f"Negative: {n}"
case n if n % 2 == 0:
return f"Positive even: {n}"
case n:
return f"Positive odd: {n}"
for num in [0, -5, 4, 7]:
print(f" {num}: {classify_number(num)}")
def validate_coordinates(coords: tuple) -> str:
match coords:
case (lat, lon) if -90 <= lat <= 90 and -180 <= lon <= 180:
return f"Valid coordinates: ({lat}, {lon})"
case (lat, lon):
return f"Out of range: lat={lat}, lon={lon}"
case _:
return "Invalid format"
print(validate_coordinates((37.5, 126.9))) # Seoul
print(validate_coordinates((100, 0))) # Latitude out of range
Real-world Example 1: Command Parser
from dataclasses import dataclass
from typing import Any
@dataclass
class Command:
name: str
args: list[Any]
flags: dict[str, Any]
def parse_and_execute(cmd: Command) -> str:
match cmd:
case Command(name="help", args=[], flags={}):
return "Available commands: help, quit, create, delete, list"
case Command(name="quit" | "exit", args=_, flags=_):
return "Quitting program"
case Command(name="create", args=[name], flags={"type": item_type}):
return f"Create '{name}' of type {item_type}"
case Command(name="create", args=[name], flags={}):
return f"Create '{name}' with default type"
case Command(name="delete", args=[name], flags={"force": True}):
return f"Force delete '{name}'"
case Command(name="delete", args=[name], flags=_):
return f"Delete '{name}' (confirmation required)"
case Command(name="list", args=[], flags={"verbose": True}):
return "Show verbose listing"
case Command(name="list", args=_, flags=_):
return "Show brief listing"
case Command(name=unknown_cmd):
return f"Unknown command: {unknown_cmd}"
commands = [
Command("help", [], {}),
Command("create", ["my_file"], {"type": "document"}),
Command("delete", ["old_data"], {"force": True}),
Command("list", [], {"verbose": True}),
]
for cmd in commands:
print(f" {cmd.name}: {parse_and_execute(cmd)}")
Real-world Example 2: AST Node Evaluation
from dataclasses import dataclass
from typing import Union
@dataclass
class Number:
value: float
@dataclass
class BinaryOp:
op: str
left: "Expr"
right: "Expr"
@dataclass
class UnaryOp:
op: str
operand: "Expr"
Expr = Union[Number, BinaryOp, UnaryOp]
def evaluate(expr: Expr) -> float:
match expr:
case Number(value=v):
return v
case UnaryOp(op="-", operand=e):
return -evaluate(e)
case BinaryOp(op="+", left=l, right=r):
return evaluate(l) + evaluate(r)
case BinaryOp(op="-", left=l, right=r):
return evaluate(l) - evaluate(r)
case BinaryOp(op="*", left=l, right=r):
return evaluate(l) * evaluate(r)
case BinaryOp(op="/", left=l, right=r):
divisor = evaluate(r)
if divisor == 0:
raise ZeroDivisionError("Cannot divide by zero")
return evaluate(l) / divisor
case BinaryOp(op=op):
raise ValueError(f"Unsupported operator: {op}")
case _:
raise TypeError(f"Unknown node type: {type(expr)}")
# (3 + 4) * 2
expr = BinaryOp("*",
BinaryOp("+", Number(3), Number(4)),
Number(2)
)
print(f"(3 + 4) * 2 = {evaluate(expr)}") # 14.0
# -(10 / 2)
expr2 = UnaryOp("-", BinaryOp("/", Number(10), Number(2)))
print(f"-(10 / 2) = {evaluate(expr2)}") # -5.0
Pro Tips
1. __match_args__ for positional class pattern order
class Color:
__match_args__ = ("red", "green", "blue")
def __init__(self, red: int, green: int, blue: int):
self.red = red
self.green = green
self.blue = blue
color = Color(255, 0, 0)
match color:
case Color(255, 0, 0):
print("Red")
case Color(0, 255, 0):
print("Green")
case Color(r, g, b):
print(f"Other color: rgb({r}, {g}, {b})")
2. AS pattern: name the matched value
def process(data):
match data:
case {"items": [first, *_] as items_list}:
print(f"First item: {first}, all: {items_list}")
case [int() | float() as num]:
print(f"Single number: {num}")
process({"items": [1, 2, 3]})
process([42])
3. Performance Note
match-case internally operates similarly to if-elif. However, class patterns and mapping patterns involve type checks and attribute access, so dictionary dispatch may be faster for large numbers of cases.