Sequence pattern matching

Use match/case for shape-based branching without brittle index code

Sequence pattern matching, introduced in Python 3.10 (PEP 634), lets you branch on the shape of a subject instead of writing manual length and type checks. Instead of `if len(seq) == 2 and isinstance(seq[0], str) and seq[0] == "move"`, you write `case ["move", x, y]`. CPython implements this with three bytecode operations: `MATCH_SEQUENCE` verifies the subject is a sequence (excluding str, bytes, and bytearray), `GET_LEN` checks length, and `UNPACK_SEQUENCE` binds matched elements. If any check fails, the match continues to the next case. Guards with `if` add extra conditions after a pattern matches. Names are only bound on successful match, so unused names in earlier cases do not pollute the scope. <a href="/sequences-slicing">Review slice mechanics for related sequence operations</a>.

Understand.
Visualize.
Master.

Python in Depth

An interactive engineering reference for Python internals

Quick note

Specific cases before broad fallbacks.

:)
Python version

Targets Python 3.10–3.14. Python 3.9 and below are End-of-Life.

TABLE OF CONTENTS
1.2Sequence pattern matching

Use match/case for shape-based branching without brittle index code

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 dataclass
from shlex import split
@dataclass(frozen=True, slots=True)
class Command:
action: str
target: str
retries: int = 0
def 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"))
Step Through Pattern Matching

See how Python tests cases in order, rejects the wrong shape, binds names from the matching shape, and only then runs the selected branch.

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 dataclass
from dis import dis
@dataclass(frozen=True, slots=True)
class PaymentEvent:
kind: str
amount_cents: int
currency: str
def 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: str
def 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 dataclass
from enum import Enum
class Status(Enum):
READY = "ready"
FAILED = "failed"
@dataclass(frozen=True, slots=True)
class Job:
status: Status
attempts: int
def 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.

Further reading

  • Official docs: match statements
  • PEP 634: structural pattern matching specification
  • PEP 635: structural pattern matching motivation
  • PEP 636: structural pattern matching tutorial
  • CPython source: pattern-matching bytecodes
BOARD NOTESContext
WHY NO BENCHMARK?

This topic is better taught with structure, semantics, and cross-references than with a synthetic chart.

Specific cases before broad fallbacks.

RELATED GUIDES
NEXT CHECKS
Contribute