Structural pattern matching is useful when branching depends on shape, not when a long if chain merely happens to compare constants. Treat match as protocol-driven destructuring with explicit failure paths.
Core answer
Use sequence and class patterns when a command, event, or parsed message has a small set of stable shapes. Bind the parts once, attach guards to semantic constraints, and keep the fallback case explicit.
# [CURRENT - 3.10-3.14] Requires Python 3.10+ [PEP 634]from dataclasses import dataclassfrom shlex import split@dataclass(frozen=True, slots=True)class Command: action: str target: str retries: int = 0def parse_command(raw: str) -> Command: match split(raw): case ["sync", target]: return Command("sync", target) case ["sync", target, "--retries", retries] if retries.isdecimal(): return Command("sync", target, int(retries)) case _: raise ValueError(f"unsupported command: {raw!r}")print(parse_command("sync invoices --retries 2"))Why this design exists
PEP 634 defines the semantics, while PEP 635 explains the motivation: a branch can match structure and bind names in one expression instead of scattering length checks, indexing, casts, and temporary names across a ladder. That design matters most for parsers, AST-like data, wire messages, and state transitions.
Pattern matching is deliberately not a general switch statement. A bare name captures instead of comparing a variable, mapping patterns test keys rather than exact mapping size, and guards run only after the structural match has succeeded. Those rules make binding precise, but they punish readers who expect C-style case semantics.
Mechanics and CPython internals
Sequence patterns ask whether the subject is considered a sequence by the pattern machinery; strings and bytes are intentionally excluded from sequence-pattern decomposition. Class patterns use the data model. Dataclasses generate __match_args__ by default for positional class patterns, and keyword class patterns read attributes. Exact opcode names and specialization details vary across CPython versions, but CPython compiles match into dedicated matching operations and normal interpreter control flow.
# [CURRENT - 3.10-3.14] Requires Python 3.10+ [PEP 634]from dataclasses import dataclassfrom dis import dis@dataclass(frozen=True, slots=True)class PaymentEvent: kind: str amount_cents: int currency: strdef route(event: PaymentEvent) -> str: match event: case PaymentEvent("captured", amount, "USD") if amount > 0: return "ledger" case PaymentEvent(kind="failed"): return "retry" case _: return "dead-letter"print(route(PaymentEvent("captured", 4200, "USD")))dis(route)Complexity and tradeoffs
Matching is not a hash-table jump for arbitrary patterns. It performs the tests needed by each case: sequence length checks, element access, attribute access, guard evaluation, and nested matches. The practical cost depends on the shape and order of cases. Use simpler early cases when they are also the most common and most readable.
The tradeoff is clarity against implicit protocol behavior. A match that reveals message shape is easier to review than fragile indexes. A match that hides ordinary business predicates behind six nested cases is harder to evolve than named helper functions.
Idiomatic patterns and refactoring
Refactor shape checks into a match when the destructuring itself is the decision. Keep validation separate when values need normalization, security checks, or error aggregation.
# [CURRENT - 3.10-3.14] Requires Python 3.10+ [PEP 634]from dataclasses import dataclass@dataclass(frozen=True, slots=True)class Route: queue: str account_id: strdef route_indexes(parts: list[str]) -> Route: if len(parts) == 3 and parts[0] == "account" and parts[2] == "refresh": return Route("refresh", parts[1]) raise ValueError(parts)def route_pattern(parts: list[str]) -> Route: match parts: case ["account", account_id, "refresh"]: return Route("refresh", account_id) case _: raise ValueError(parts)print(route_indexes(["account", "A-42", "refresh"]))print(route_pattern(["account", "A-42", "refresh"]))Common mistakes and edge cases
Do not write case expected: expecting a comparison against a local variable; that name captures. Use a value pattern such as an enum member, a qualified constant, or a guard. Do not rely on positional class patterns when public field order is unstable; keyword patterns make the contract clearer.
# [CURRENT - 3.10-3.14] Requires Python 3.10+ [PEP 634]from dataclasses import dataclassfrom enum import Enumclass Status(Enum): READY = "ready" FAILED = "failed"@dataclass(frozen=True, slots=True)class Job: status: Status attempts: intdef choose(job: Job) -> str: match job: case Job(status=Status.READY, attempts=attempts) if attempts < 3: return "run" case Job(status=Status.FAILED): return "inspect" case _: return "hold"print(choose(Job(Status.READY, 1)))When to use / When NOT to use
Use match when stable structural alternatives are the hard part of the code and name binding improves the branch. Use if statements when the decision is a small predicate over already named values.
Do not use pattern matching to replace every comparison ladder, to validate untrusted data silently, or to hide a large state machine without tests for every fallback path.