Structural Pattern Matching with match and case

Updated

September 7, 2026

Structural Pattern Matching with match and case

After reading this chapter, you will master Python’s Structural Pattern Matching syntax (match and case introduced in PEP 634), destructure sequence, mapping, and class patterns, enforce conditional guards with if, and prevent common variable-capture traps.

Mental model

Traditional if/elif statements evaluate boolean expressions one by one. Structural Pattern Matching evaluates data against a structural shape specification: testing type, structure, and content simultaneously, while extracting and binding variables on success:

Incoming Data: {"action": "scale", "service": "auth", "replicas": 5}
                    │
                    ▼
          [ match expression ]
                    │
                    ├─ case {"action": "restart", "service": str(svc)} ──▶ No match (action != restart)
                    │
                    ├─ case {"action": "scale", "service": str(svc), "replicas": int(n)} if n > 0
                    │       │
                    │       ├─ Matches keys: "action", "service", "replicas"
                    │       ├─ Type matches: str and int
                    │       ├─ Guard condition passes: n (5) > 0
                    │       └─ Binds: svc = "auth", n = 5
                    │       └────────────────────────────────────────────▶ Execute Branch!
                    │
                    └─ case _ ──▶ Wildcard Fallback

Minimal example

Save as pattern_matching_dispatch.py:

# pattern_matching_dispatch.py
def process_command(cmd: list[str]) -> str:
    """Parse a CLI token list into an actionable response using sequence patterns."""
    match cmd:
        case ["quit" | "exit"]:
            return "Terminating process."
        case ["get", str(key)]:
            return f"Retrieving key: {key}"
        case ["set", str(key), value]:
            return f"Setting key: {key} = {value}"
        case ["batch", *items] if len(items) > 0:
            return f"Processing batch of {len(items)} items: {', '.join(items)}"
        case _:
            return f"Invalid or malformed command: {cmd}"

def main() -> None:
    test_commands = [
        ["quit"],
        ["get", "redis.host"],
        ["set", "timeout", "30s"],
        ["batch", "item1", "item2", "item3"],
        ["unknown", "foo", "bar"],
    ]
    for command in test_commands:
        result = process_command(command)
        print(f"Command: {command} -> Result: {result}")

if __name__ == "__main__":
    main()

Run via uv run python pattern_matching_dispatch.py:

Command: ['quit'] -> Result: Terminating process.
Command: ['get', 'redis.host'] -> Result: Retrieving key: redis.host
Command: ['set', 'timeout', '30s'] -> Result: Setting key: timeout = 30s
Command: ['batch', 'item1', 'item2', 'item3'] -> Result: Processing batch of 3 items: item1, item2, item3
Command: ['unknown', 'foo', 'bar'] -> Result: Invalid or malformed command: ['unknown', 'foo', 'bar']

Worked examples

Case 1: Mapping Patterns for Telemetry Event Routing

Mapping patterns allow inspecting structured dictionaries (such as JSON payloads from message queues) and extracting specific keys:

# telemetry_router.py
from typing import Any

def handle_telemetry_event(event: dict[str, Any]) -> None:
    match event:
        case {"type": "metric", "name": str(name), "value": float(val)} if val >= 90.0:
            print(f"[HIGH WATERMARK] Metric {name} breached alert threshold: {val}%")
        
        case {"type": "metric", "name": str(name), "value": float(val)}:
            print(f"[NOMINAL] Metric {name} at {val}%")
        
        case {"type": "heartbeat", "node_id": str(node), "status": "alive"}:
            print(f"[HEARTBEAT] Node {node} reported healthy.")
        
        case {"type": "alert", "severity": "CRITICAL", "message": str(msg)}:
            print(f"[PAGERDUTY ALERT] Critical incident: {msg}")
        
        case _:
            print(f"[UNHANDLED] Discarding unrecognized event structure: {event}")

if __name__ == "__main__":
    events = [
        {"type": "metric", "name": "cpu_utilization", "value": 94.2},
        {"type": "heartbeat", "node_id": "k8s-worker-04", "status": "alive", "uptime": 86400},
        {"type": "alert", "severity": "CRITICAL", "message": "Power rail offline"},
        {"source": "unknown", "data": None},
    ]

    for ev in events:
        handle_telemetry_event(ev)

Run:

uv run python telemetry_router.py

Output:

[HIGH WATERMARK] Metric cpu_utilization breached alert threshold: 94.2%
[HEARTBEAT] Node k8s-worker-04 reported healthy.
[PAGERDUTY ALERT] Critical incident: Power rail offline
[UNHANDLED] Discarding unrecognized event structure: {'source': 'unknown', 'data': None}

Notice that the heartbeat event matched despite containing an extra key ("uptime": 86400). In Python mapping patterns, extra keys are ignored unless explicitly restricted with **_ or full length checks.

Case 2: Class Patterns with Dataclasses

Pattern matching works natively with object-oriented classes and dataclasses:

# packet_matcher.py
from dataclasses import dataclass

@dataclass
class Packet:
    source_ip: str
    dest_port: int
    payload_len: int
    protocol: str = "TCP"

def inspect_traffic(packet: Packet) -> str:
    match packet:
        # Match port 22 or 3389 with payload
        case Packet(dest_port=22 | 3389, payload_len=length) if length > 0:
            return f"Remote administration session to port {packet.dest_port} ({length} bytes)"
        
        # Match web traffic
        case Packet(dest_port=80 | 443):
            return f"Standard Web Traffic on port {packet.dest_port}"
        
        # Match UDP DNS traffic
        case Packet(protocol="UDP", dest_port=53):
            return "DNS Query Traffic"
        
        case Packet(dest_port=port):
            return f"General traffic on port {port}"

if __name__ == "__main__":
    packets = [
        Packet("192.168.1.100", 22, 512),
        Packet("10.0.0.5", 443, 1024),
        Packet("172.16.0.2", 53, 64, protocol="UDP"),
    ]

    for p in packets:
        print(f"{p.source_ip} -> {inspect_traffic(p)}")

Run:

uv run python packet_matcher.py

Output:

192.168.1.100 -> Remote administration session to port 22 (512 bytes)
10.0.0.5 -> Standard Web Traffic on port 443
172.16.0.2 -> DNS Query Traffic

Pitfalls

Pitfall 1: The Variable Capture Trap (Constants vs Variables)

In a case clause, an unqualified name (like status) is treated as a capture variable, NOT a value equality check!

# THE TRAP:
OK = 200
response_code = 404

match response_code:
    case OK:  # BUG: This does NOT compare to OK! It assigns response_code to a new variable called 'OK'!
        print("This ALWAYS prints because 'OK' matches anything and gets bound!")

# THE FIX: Use an Enum or dotted attribute
class Status:
    OK = 200

match response_code:
    case Status.OK:  # Safe: Dotted names are looked up by value, not treated as capture variables
        print("HTTP 200 OK")
    case _:
        print("Other status")

Exercises

  1. Create a match ... case statement that parses a network URI string decomposed into [scheme, host, port], handling default ports (80 for http, 443 for https) when port is omitted.
  2. Define a dataclass DeploymentPlan(env: str, replicas: int, dry_run: bool). Write a pattern matching function that forbids any deployment to env="production" if replicas > 10 and dry_run is False.
  3. Demonstrate the difference between case [head, *tail]: and case [head, tail]: when passed a list with four elements.
  4. Write a pattern matching function that accepts an arbitrary JSON-like value and returns whether it is a scalar, a list of strings, or a dictionary mapping strings to integers.

Further reading

  • PEP 634: Structural Pattern Matching: Specification.
  • PEP 635: Structural Pattern Matching: Motivation and Rationale.
  • PEP 636: Structural Pattern Matching: Tutorial.