14. Pattern Matching
14. Pattern Matching¶
Learning Objectives¶
- Understand Python 3.10+ match/case syntax
- Master various pattern types
- Use structural pattern matching
- Apply guards and OR patterns
- Learn practical usage patterns
Table of Contents¶
- Pattern Matching Basics
- Literal Patterns
- Structural Patterns
- Class Patterns
- Guards and OR Patterns
- Practical Applications
- Practice Problems
1. Pattern Matching Basics¶
1.1 Introducing match/case¶
# Requires Python 3.10+
def http_status(status: int) -> str:
match status:
case 200:
return "OK"
case 404:
return "Not Found"
case 500:
return "Internal Server Error"
case _: # Wildcard (default)
return f"Unknown status: {status}"
print(http_status(200)) # OK
print(http_status(404)) # Not Found
print(http_status(999)) # Unknown status: 999
1.2 Comparison with if-elif¶
# if-elif approach
def get_day_type_if(day: str) -> str:
if day in ("Saturday", "Sunday"):
return "Weekend"
elif day in ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday"):
return "Weekday"
else:
return "Invalid day"
# match/case approach
def get_day_type_match(day: str) -> str:
match day:
case "Saturday" | "Sunday": # OR pattern
return "Weekend"
case "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday":
return "Weekday"
case _:
return "Invalid day"
1.3 Variable Capture¶
def describe_point(point):
match point:
case (0, 0):
return "Origin"
case (x, 0): # Capture x
return f"On X-axis at x={x}"
case (0, y): # Capture y
return f"On Y-axis at y={y}"
case (x, y): # Capture both
return f"Point at ({x}, {y})"
case _:
return "Not a point"
print(describe_point((0, 0))) # Origin
print(describe_point((5, 0))) # On X-axis at x=5
print(describe_point((0, 3))) # On Y-axis at y=3
print(describe_point((2, 4))) # Point at (2, 4)
2. Literal Patterns¶
2.1 Various Literals¶
def check_value(value):
match value:
# Number literals
case 0:
return "Zero"
case 1 | 2 | 3: # OR pattern
return "Small positive"
# String literals
case "":
return "Empty string"
case "hello":
return "Greeting"
# Boolean
case True:
return "True value"
case False:
return "False value"
# None
case None:
return "None value"
case _:
return "Other"
print(check_value(0)) # Zero
print(check_value(2)) # Small positive
print(check_value("hello")) # Greeting
print(check_value(None)) # None value
2.2 Constant Comparison¶
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
def describe_color(color: Color) -> str:
match color:
case Color.RED:
return "Stop"
case Color.GREEN:
return "Go"
case Color.BLUE:
return "Cool"
case _:
return "Unknown color"
print(describe_color(Color.RED)) # Stop
print(describe_color(Color.GREEN)) # Go
2.3 Constant Pattern (Dot Notation)¶
# Constants in patterns must include dot (.)
class HttpStatus:
OK = 200
NOT_FOUND = 404
ERROR = 500
def check_status(status: int) -> str:
match status:
case HttpStatus.OK:
return "Success"
case HttpStatus.NOT_FOUND:
return "Not found"
case HttpStatus.ERROR:
return "Server error"
case _:
return "Unknown"
# Simple variables are interpreted as capture variables, not constants
# OK = 200
# case OK: # This is NOT comparing to 200, it's capturing!
3. Structural Patterns¶
3.1 Sequence Patterns¶
def analyze_sequence(seq):
match seq:
case []:
return "Empty sequence"
case [single]:
return f"Single element: {single}"
case [first, second]:
return f"Two elements: {first}, {second}"
case [first, *middle, last]: # Unpacking
return f"First: {first}, Middle: {middle}, Last: {last}"
print(analyze_sequence([])) # Empty sequence
print(analyze_sequence([1])) # Single element: 1
print(analyze_sequence([1, 2])) # Two elements: 1, 2
print(analyze_sequence([1, 2, 3, 4])) # First: 1, Middle: [2, 3], Last: 4
3.2 Dictionary Patterns¶
def process_event(event: dict):
match event:
case {"type": "click", "x": x, "y": y}:
return f"Click at ({x}, {y})"
case {"type": "keypress", "key": key}:
return f"Key pressed: {key}"
case {"type": "scroll", "direction": direction, **rest}:
return f"Scroll {direction}, extra: {rest}"
case {"type": event_type}:
return f"Unknown event type: {event_type}"
case _:
return "Invalid event"
print(process_event({"type": "click", "x": 100, "y": 200}))
# Click at (100, 200)
print(process_event({"type": "keypress", "key": "Enter"}))
# Key pressed: Enter
print(process_event({"type": "scroll", "direction": "down", "speed": 10}))
# Scroll down, extra: {'speed': 10}
3.3 Nested Structures¶
def process_response(response: dict):
match response:
case {"status": "success", "data": {"users": [first_user, *_]}}:
return f"First user: {first_user}"
case {"status": "success", "data": {"count": count}}:
return f"Count: {count}"
case {"status": "error", "error": {"code": code, "message": msg}}:
return f"Error {code}: {msg}"
case {"status": status}:
return f"Status: {status}"
response1 = {"status": "success", "data": {"users": ["Alice", "Bob"]}}
print(process_response(response1)) # First user: Alice
response2 = {"status": "error", "error": {"code": 404, "message": "Not found"}}
print(process_response(response2)) # Error 404: Not found
4. Class Patterns¶
4.1 Dataclasses and Pattern Matching¶
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Circle:
center: Point
radius: float
@dataclass
class Rectangle:
top_left: Point
width: float
height: float
def describe_shape(shape):
match shape:
case Point(x=0, y=0):
return "Origin point"
case Point(x=x, y=y):
return f"Point at ({x}, {y})"
case Circle(center=Point(x=0, y=0), radius=r):
return f"Circle at origin with radius {r}"
case Circle(center=c, radius=r):
return f"Circle at ({c.x}, {c.y}) with radius {r}"
case Rectangle(width=w, height=h) if w == h:
return f"Square with side {w}"
case Rectangle(top_left=tl, width=w, height=h):
return f"Rectangle at ({tl.x}, {tl.y}), {w}x{h}"
case _:
return "Unknown shape"
print(describe_shape(Point(0, 0))) # Origin point
print(describe_shape(Point(3, 4))) # Point at (3, 4)
print(describe_shape(Circle(Point(0, 0), 5))) # Circle at origin with radius 5
print(describe_shape(Rectangle(Point(1, 2), 10, 10))) # Square with side 10
4.2 Positional Argument Pattern (match_args)¶
from dataclasses import dataclass
@dataclass
class Vector:
x: float
y: float
z: float = 0.0
# dataclass automatically generates __match_args__
def describe_vector(v):
match v:
case Vector(0, 0, 0): # Match with positional args
return "Zero vector"
case Vector(x, 0, 0):
return f"X-axis vector: {x}"
case Vector(0, y, 0):
return f"Y-axis vector: {y}"
case Vector(0, 0, z):
return f"Z-axis vector: {z}"
case Vector(x, y, z):
return f"Vector ({x}, {y}, {z})"
print(describe_vector(Vector(0, 0, 0))) # Zero vector
print(describe_vector(Vector(5, 0, 0))) # X-axis vector: 5
print(describe_vector(Vector(1, 2, 3))) # Vector (1, 2, 3)
4.3 Regular Classes¶
class Animal:
__match_args__ = ("name", "species")
def __init__(self, name: str, species: str):
self.name = name
self.species = species
class Dog(Animal):
__match_args__ = ("name", "breed")
def __init__(self, name: str, breed: str):
super().__init__(name, "dog")
self.breed = breed
def greet_animal(animal):
match animal:
case Dog(name, breed="Labrador"):
return f"Good dog, {name}! Labs are the best!"
case Dog(name, breed):
return f"Hello, {name} the {breed}!"
case Animal(name, species):
return f"Hello, {name} the {species}!"
print(greet_animal(Dog("Buddy", "Labrador"))) # Good dog, Buddy!
print(greet_animal(Dog("Max", "Beagle"))) # Hello, Max the Beagle!
print(greet_animal(Animal("Whiskers", "cat"))) # Hello, Whiskers the cat!
5. Guards and OR Patterns¶
5.1 Guards (if conditions)¶
def categorize_number(n: int) -> str:
match n:
case n if n < 0:
return "Negative"
case 0:
return "Zero"
case n if n % 2 == 0:
return "Positive even"
case n if n % 2 == 1:
return "Positive odd"
case _:
return "Unknown"
print(categorize_number(-5)) # Negative
print(categorize_number(0)) # Zero
print(categorize_number(4)) # Positive even
print(categorize_number(7)) # Positive odd
5.2 Complex Guards¶
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
role: str
def check_access(user: User, resource: str) -> str:
match (user, resource):
case (User(role="admin"), _):
return "Full access"
case (User(age=age), "adult_content") if age < 18:
return "Access denied: Age restriction"
case (User(role="user"), "admin_panel"):
return "Access denied: Admin only"
case (User(name=name), resource):
return f"{name} can access {resource}"
admin = User("Alice", 30, "admin")
teen = User("Bob", 16, "user")
user = User("Charlie", 25, "user")
print(check_access(admin, "admin_panel")) # Full access
print(check_access(teen, "adult_content")) # Access denied: Age restriction
print(check_access(user, "admin_panel")) # Access denied: Admin only
print(check_access(user, "profile")) # Charlie can access profile
5.3 OR Pattern (|)¶
def classify_char(char: str) -> str:
match char:
case 'a' | 'e' | 'i' | 'o' | 'u':
return "Lowercase vowel"
case 'A' | 'E' | 'I' | 'O' | 'U':
return "Uppercase vowel"
case ' ' | '\t' | '\n':
return "Whitespace"
case '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9':
return "Digit"
case _:
return "Other"
print(classify_char('a')) # Lowercase vowel
print(classify_char('E')) # Uppercase vowel
print(classify_char(' ')) # Whitespace
print(classify_char('5')) # Digit
print(classify_char('x')) # Other
5.4 AS Pattern (Alias)¶
def process_command(command):
match command:
case ["quit" | "exit" | "q" as cmd]:
return f"Exit command: {cmd}"
case ["load", filename] | ["open", filename] as full_cmd:
return f"Loading file: {filename} (command: {full_cmd})"
case [("get" | "fetch") as action, *args]:
return f"Action: {action}, Args: {args}"
case _:
return "Unknown command"
print(process_command(["quit"])) # Exit command: quit
print(process_command(["exit"])) # Exit command: exit
print(process_command(["load", "data.txt"])) # Loading file: data.txt
print(process_command(["get", "user", "123"])) # Action: get, Args: ['user', '123']
6. Practical Applications¶
6.1 JSON API Response Handling¶
from typing import Any
def handle_api_response(response: dict[str, Any]) -> str:
match response:
case {"status": 200, "data": {"items": [first, *rest]}}:
return f"Success: First item = {first}, {len(rest)} more items"
case {"status": 200, "data": data}:
return f"Success: {data}"
case {"status": 400, "error": {"field": field, "message": msg}}:
return f"Validation error in '{field}': {msg}"
case {"status": 401}:
return "Unauthorized: Please login"
case {"status": 403, "error": {"reason": reason}}:
return f"Forbidden: {reason}"
case {"status": 404}:
return "Not found"
case {"status": status} if 500 <= status < 600:
return f"Server error: {status}"
case {"status": status}:
return f"Unknown status: {status}"
case _:
return "Invalid response format"
# Test
responses = [
{"status": 200, "data": {"items": ["a", "b", "c"]}},
{"status": 400, "error": {"field": "email", "message": "Invalid format"}},
{"status": 401},
{"status": 500},
]
for r in responses:
print(handle_api_response(r))
6.2 State Machine¶
from dataclasses import dataclass
from enum import Enum, auto
class State(Enum):
IDLE = auto()
RUNNING = auto()
PAUSED = auto()
STOPPED = auto()
@dataclass
class Event:
pass
@dataclass
class Start(Event):
pass
@dataclass
class Pause(Event):
pass
@dataclass
class Resume(Event):
pass
@dataclass
class Stop(Event):
pass
def transition(state: State, event: Event) -> State:
match (state, event):
case (State.IDLE, Start()):
return State.RUNNING
case (State.RUNNING, Pause()):
return State.PAUSED
case (State.RUNNING, Stop()):
return State.STOPPED
case (State.PAUSED, Resume()):
return State.RUNNING
case (State.PAUSED, Stop()):
return State.STOPPED
case (state, event):
print(f"Invalid transition: {state} + {event}")
return state
# State transition test
state = State.IDLE
print(f"Initial: {state}")
state = transition(state, Start())
print(f"After Start: {state}")
state = transition(state, Pause())
print(f"After Pause: {state}")
state = transition(state, Resume())
print(f"After Resume: {state}")
state = transition(state, Stop())
print(f"After Stop: {state}")
6.3 AST Processing (Interpreter Pattern)¶
from dataclasses import dataclass
from typing import Union
@dataclass
class Num:
value: float
@dataclass
class BinOp:
left: "Expr"
op: str
right: "Expr"
@dataclass
class UnaryOp:
op: str
operand: "Expr"
Expr = Union[Num, BinOp, UnaryOp]
def evaluate(expr: Expr) -> float:
match expr:
case Num(value):
return value
case BinOp(left, "+", right):
return evaluate(left) + evaluate(right)
case BinOp(left, "-", right):
return evaluate(left) - evaluate(right)
case BinOp(left, "*", right):
return evaluate(left) * evaluate(right)
case BinOp(left, "/", right):
right_val = evaluate(right)
if right_val == 0:
raise ValueError("Division by zero")
return evaluate(left) / right_val
case UnaryOp("-", operand):
return -evaluate(operand)
case _:
raise ValueError(f"Unknown expression: {expr}")
# (3 + 4) * -2
expr = BinOp(
BinOp(Num(3), "+", Num(4)),
"*",
UnaryOp("-", Num(2))
)
print(evaluate(expr)) # -14.0
6.4 CLI Command Parser¶
import sys
def parse_command(args: list[str]) -> dict:
match args:
case []:
return {"command": "help"}
case ["--version" | "-v"]:
return {"command": "version"}
case ["--help" | "-h"]:
return {"command": "help"}
case ["init", name]:
return {"command": "init", "name": name}
case ["init", name, "--template", template]:
return {"command": "init", "name": name, "template": template}
case ["run", *files] if files:
return {"command": "run", "files": files}
case ["config", "set", key, value]:
return {"command": "config_set", "key": key, "value": value}
case ["config", "get", key]:
return {"command": "config_get", "key": key}
case [unknown, *_]:
return {"command": "error", "message": f"Unknown command: {unknown}"}
# Test
commands = [
[],
["--version"],
["init", "myproject"],
["init", "myproject", "--template", "fastapi"],
["run", "app.py", "tests.py"],
["config", "set", "debug", "true"],
["unknown", "arg"],
]
for cmd in commands:
print(f"{cmd} -> {parse_command(cmd)}")
7. Practice Problems¶
Exercise 1: Shape Area Calculator¶
Write a function that calculates areas of various shapes.
# Sample solution
from dataclasses import dataclass
import math
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
@dataclass
class Triangle:
base: float
height: float
Shape = Circle | Rectangle | Triangle
def calculate_area(shape: Shape) -> float:
match shape:
case Circle(radius=r):
return math.pi * r ** 2
case Rectangle(width=w, height=h):
return w * h
case Triangle(base=b, height=h):
return 0.5 * b * h
print(calculate_area(Circle(5))) # ~78.54
print(calculate_area(Rectangle(4, 5))) # 20
print(calculate_area(Triangle(6, 4))) # 12
Exercise 2: HTTP Request Router¶
Implement a simple HTTP request router.
# Sample solution
def route_request(method: str, path: str) -> str:
match (method, path.split("/")):
case ("GET", ["", ""]):
return "Home page"
case ("GET", ["", "users"]):
return "List users"
case ("GET", ["", "users", user_id]):
return f"Get user {user_id}"
case ("POST", ["", "users"]):
return "Create user"
case ("PUT", ["", "users", user_id]):
return f"Update user {user_id}"
case ("DELETE", ["", "users", user_id]):
return f"Delete user {user_id}"
case (method, _):
return f"404 Not Found: {method} {path}"
print(route_request("GET", "/")) # Home page
print(route_request("GET", "/users")) # List users
print(route_request("GET", "/users/123")) # Get user 123
print(route_request("POST", "/users")) # Create user
print(route_request("DELETE", "/users/456")) # Delete user 456
Exercise 3: Recursive Tree Traversal¶
Traverse a tree structure using pattern matching.
# Sample solution
from dataclasses import dataclass
from typing import Optional
@dataclass
class TreeNode:
value: int
left: Optional["TreeNode"] = None
right: Optional["TreeNode"] = None
def sum_tree(node: Optional[TreeNode]) -> int:
match node:
case None:
return 0
case TreeNode(value=v, left=l, right=r):
return v + sum_tree(l) + sum_tree(r)
tree = TreeNode(
1,
TreeNode(2, TreeNode(4), TreeNode(5)),
TreeNode(3, None, TreeNode(6))
)
print(sum_tree(tree)) # 21 (1+2+3+4+5+6)