Python 3.10: Pattern Matching with Match/Case

python dev

Python 3.10 brings structural pattern matching—Python’s take on switch/case. But it’s much more than a switch statement. Here’s what you need to know.

Basic Syntax

def http_status(status):
    match status:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500:
            return "Internal Server Error"
        case _:
            return "Unknown"

The _ is the wildcard pattern—it matches anything.

Beyond Switch Statements

Matching Sequences

def handle_point(point):
    match point:
        case (0, 0):
            return "Origin"
        case (0, y):
            return f"Y-axis at {y}"
        case (x, 0):
            return f"X-axis at {x}"
        case (x, y):
            return f"Point at ({x}, {y})"

handle_point((0, 0))    # "Origin"
handle_point((0, 5))    # "Y-axis at 5"
handle_point((3, 4))    # "Point at (3, 4)"

Variable Capture

Variables in patterns capture the matched value:

def process_command(command):
    match command.split():
        case ["quit"]:
            return "Goodbye"
        case ["load", filename]:
            return f"Loading {filename}"
        case ["save", filename]:
            return f"Saving to {filename}"
        case ["move", direction, distance]:
            return f"Moving {direction} by {distance}"
        case _:
            return "Unknown command"

process_command("load data.csv")      # "Loading data.csv"
process_command("move north 10")      # "Moving north by 10"

Matching Dictionaries

def handle_event(event):
    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": dir}:
            return f"Scroll {dir}"
        case _:
            return "Unknown event"

handle_event({"type": "click", "x": 100, "y": 200})  # "Click at (100, 200)"

Class Patterns

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

@dataclass
class Circle:
    center: Point
    radius: int

@dataclass
class Rectangle:
    top_left: Point
    bottom_right: Point

def describe_shape(shape):
    match shape:
        case Circle(center=Point(0, 0), radius=r):
            return f"Circle at origin with radius {r}"
        case Circle(center=c, radius=r):
            return f"Circle at {c} with radius {r}"
        case Rectangle(top_left=Point(x1, y1), bottom_right=Point(x2, y2)):
            return f"Rectangle from ({x1},{y1}) to ({x2},{y2})"
        case _:
            return "Unknown shape"

Guards

Add conditions with if:

def classify_number(n):
    match n:
        case n if n < 0:
            return "Negative"
        case 0:
            return "Zero"
        case n if n % 2 == 0:
            return "Positive even"
        case _:
            return "Positive odd"

OR Patterns

Match multiple patterns:

def handle_response(response):
    match response:
        case 200 | 201 | 204:
            return "Success"
        case 400 | 401 | 403 | 404:
            return "Client error"
        case 500 | 502 | 503:
            return "Server error"
        case _:
            return "Unknown"

AS Pattern

Capture while matching:

def process_data(data):
    match data:
        case {"user": {"name": name, "email": email} as user_data}:
            return f"User: {name}, Full data: {user_data}"

Real-World Examples

API Response Handling

def handle_api_response(response):
    match response:
        case {"status": "success", "data": data}:
            return process_data(data)
        case {"status": "error", "code": code, "message": msg}:
            log_error(code, msg)
            return None
        case {"status": "pending", "job_id": job_id}:
            return poll_job(job_id)
        case _:
            raise ValueError("Invalid response format")

Command-Line Argument Parsing

def parse_args(args):
    match args:
        case []:
            return "No arguments"
        case ["-h"] | ["--help"]:
            return show_help()
        case ["-v"] | ["--version"]:
            return show_version()
        case ["-f", filename]:
            return process_file(filename)
        case ["-f", filename, "-o", output]:
            return process_file(filename, output)
        case _:
            return "Invalid arguments"

State Machine

@dataclass
class State:
    name: str
    data: dict = None

def handle_state(state, event):
    match (state.name, event):
        case ("idle", "start"):
            return State("running")
        case ("running", "pause"):
            return State("paused")
        case ("paused", "resume"):
            return State("running")
        case ("running" | "paused", "stop"):
            return State("idle")
        case _:
            return state  # No transition

JSON Parsing

def parse_config(config):
    match config:
        case {"database": {"host": host, "port": port, **db_opts}}:
            return DatabaseConfig(host, port, **db_opts)
        case {"database": str() as url}:
            return DatabaseConfig.from_url(url)
        case {"database": None}:
            return DatabaseConfig.default()
        case _:
            raise ConfigError("Invalid database configuration")

Pattern Types Summary

PatternExampleMatches
Literal42, "hello"Exact value
Capturex, nameAny value, binds to variable
Wildcard_Any value, no binding
Sequence[a, b, c]Lists/tuples with pattern
Mapping{"key": value}Dict with pattern
ClassPoint(x, y)Class instances
ORa | bEither pattern
ASpattern as namePattern + capture
Guardcase x if x > 0Pattern with condition

Tips

Use with Dataclasses

@dataclass
class User:
    name: str
    role: str
    active: bool = True

# Pattern matching works naturally
match user:
    case User(role="admin", active=True):
        return "Active admin"

Avoid Overuse

Simple if/elif often clearer:

# Match: overkill
match x:
    case 1:
        return "one"
    case 2:
        return "two"

# Better as dict
{1: "one", 2: "two"}.get(x)

Match is Exhaustive

No fallthrough; only first match executes.

Final Thoughts

Pattern matching is Python’s most significant syntax addition in years. It shines for:

Learn the patterns. Use where it clarifies. Skip where it complicates.


Match what you mean.

All posts