Python Pattern Matching: A Complete Guide to Structural Match-Case with Real-World Examples

Python’s structural pattern matching, introduced in Python 3.10 and refined through 3.13+, is one of the most powerful features modern Python offers. If you’re still writing long if-elif chains to inspect data structures, match-case statements will transform how you write Python code. In this guide, we’ll explore practical, real-world uses of pattern matching that go far beyond simple value comparisons.

What Is Structural Pattern Matching?

Pattern matching lets you match values against patterns and destructure them in a single step. Think of it as a supercharged switch statement that can inspect the shape of your data, not just its value.

match command:
    case "quit":
        sys.exit(0)
    case "hello":
        print("Hi there!")
    case _:
        print(f"Unknown command: {command}")

But this basic example barely scratches the surface. The real power comes from matching complex data structures.

Matching Dictionaries and JSON Data

One of the most practical uses of pattern matching is parsing API responses or JSON payloads. Instead of writing defensive code with .get() calls everywhere, you can match the structure directly:

def handle_webhook(event: dict):
    match event:
        case {"type": "payment", "status": "completed", "amount": amount}:
            process_payment(amount)
        case {"type": "payment", "status": "failed", "error": err}:
            log_payment_error(err)
        case {"type": "subscription", "action": "cancel", "user_id": uid}:
            cancel_subscription(uid)
        case {"type": str(t)}:
            logger.warning(f"Unhandled event type: {t}")
        case _:
            logger.error(f"Malformed event: {event}")

Each case both checks the structure and extracts the values you need. No more nested if-statements or try-except blocks for data access.

Matching with Guards

You can add conditions to patterns using if guards, combining structural checks with value checks:

def categorize_user(user: dict):
    match user:
        case {"role": "admin", "active": True}:
            return "active_admin"
        case {"role": "user", "posts": n} if n > 100:
            return "power_user"
        case {"role": "user", "posts": n} if n > 0:
            return "regular_user"
        case {"role": "user", "posts": 0}:
            return "lurker"
        case _:
            return "unknown"

Matching Class Instances

Pattern matching works beautifully with dataclasses and named tuples, making it ideal for command or event handling systems:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Circle:
    center: Point
    radius: float

@dataclass
class Rectangle:
    origin: Point
    width: float
    height: float

def describe_shape(shape):
    match shape:
        case Circle(center=Point(x=0, y=0), radius=r):
            return f"Circle at origin with radius {r}"
        case Circle(center=Point(x=x, y=y), radius=r) if r > 100:
            return f"Large circle at ({x}, {y})"
        case Rectangle(width=w, height=h) if w == h:
            return f"Square with side {w}"
        case Rectangle(origin=Point(x=x, y=y), width=w, height=h):
            return f"Rectangle {w}x{h} at ({x}, {y})"
        case _:
            return "Unknown shape"

Notice how we can nest patterns — matching a Circle whose center is a Point at the origin, all in one line.

Building a CLI Parser with Pattern Matching

Here’s a practical example — a mini command-line parser for a task management tool:

def execute(tokens: list[str]):
    match tokens:
        case ["add", *task_words]:
            task = " ".join(task_words)
            add_task(task)
            print(f"Added: {task}")
        case ["done", id_str] if id_str.isdigit():
            mark_done(int(id_str))
        case ["list"]:
            show_all_tasks()
        case ["list", "--tag", tag]:
            show_tasks_by_tag(tag)
        case ["delete", *ids] if all(i.isdigit() for i in ids):
            for i in ids:
                delete_task(int(i))
        case ["help" | "--help" | "-h"]:
            show_help()
        case []:
            show_help()
        case _:
            print(f"Unknown command: {' '.join(tokens)}")

# Usage
execute(input(">> ").split())

The ["add", *task_words] pattern captures the first token and collects the rest into a list — perfect for variable-length commands. The | (or-pattern) in the help case matches any of three alternatives.

Pattern Matching in API Route Handlers

Here’s how pattern matching can clean up a FastAPI or Flask-style request handler:

from enum import Enum

class HttpMethod(Enum):
    GET = "GET"
    POST = "POST"
    PUT = "PUT"
    DELETE = "DELETE"

def route(method: HttpMethod, path: list[str], body: dict | None):
    match (method, path, body):
        case (HttpMethod.GET, ["users"], _):
            return list_users()
        case (HttpMethod.GET, ["users", user_id], _):
            return get_user(user_id)
        case (HttpMethod.POST, ["users"], {"name": name, "email": email}):
            return create_user(name, email)
        case (HttpMethod.PUT, ["users", uid], {"name": str(name)}):
            return update_user(uid, name)
        case (HttpMethod.DELETE, ["users", uid], _):
            return delete_user(uid)
        case _:
            return {"error": "Not found"}, 404

By matching a tuple of (method, path, body), we create a clean, readable routing table that also validates the request body structure.

Common Mistakes to Avoid

  • Forgetting the wildcard: Always include a case _: default to catch unmatched patterns. Without it, unmatched values silently pass through.
  • Variable capture confusion: In patterns, bare names like x are capture variables, not comparisons. To match a constant, use dotted names (HttpMethod.GET) or literal values.
  • Order matters: More specific patterns must come before general ones. Python evaluates cases top to bottom and takes the first match.
  • Dict matching is partial: {"type": "payment"} matches any dict that contains that key-value pair, even if the dict has more keys. This is intentional and useful.

Performance Considerations

Pattern matching in Python is syntactic sugar — it doesn’t compile to a jump table like switch statements in C. For simple value matching, performance is comparable to if-elif chains. The advantage is readability and correctness, especially with complex nested structures.

For hot loops matching simple values, an if-elif chain or dictionary dispatch may be marginally faster. But for anything involving data destructuring, pattern matching wins on both clarity and maintainability.

Wrapping Up

Structural pattern matching is one of Python’s best modern features. It shines brightest when you’re working with heterogeneous data — API responses, command parsing, event handling, or any code that needs to inspect and destructure complex objects. Start using it in your next project, and those tangled if-elif chains will become a thing of the past.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Privacy Policy · Contact · Sitemap

© 7Tech – Programming and Tech Tutorials