Python

Python Pattern Matching👨‍💻

Structural Pattern Matching (PEP 634, Python 3.10+) is the single biggest change to Python's control flow since generators. It's not a switch statement — it's a way to branch on the shape of data. You hand it a value, and it destructures it against a series of patterns, binding variables along the way.

If you've ever written a chain of isinstance() checks, nested if/elif blocks testing dictionary keys, or manual tuple unpacking to dispatch on data shape, pattern matching replaces all of that with something the reader can parse in one pass. It shines hardest in code that routes on data: HTTP handlers, event dispatchers, protocol parsers, AST walkers, anything where the logic is "look at what this thing is, then act accordingly."

Key Takeaways

  • 1Pattern matching branches on data shape, not just value — it destructures and binds variables in one step
  • 2Literal patterns match exact values; OR patterns (`|`) combine alternatives without repeating case bodies
  • 3Capture patterns bind matched values to names you use in the case body — no extra assignment needed
  • 4Guard clauses (`if` after a pattern) add runtime conditions when the structural match alone isn't enough
  • 5Class patterns match dataclass and NamedTuple instances by attribute, replacing chains of `isinstance()` + attribute access
  • 6Mapping patterns match dictionaries by key presence and extract values — ideal for JSON/API response handling

Master python pattern matching

Take the Python Fundamentals course with hands-on lessons and challenges.

Examples

HTTP request routing with literal and OR patterns

python

Each case matches a Request dataclass by its attributes. OR patterns collapse GET|HEAD into one branch. Guard clauses validate the path prefix and body presence. The final case captures an unknown method for a clean 405 response.

Parsing nested JSON API responses with mapping patterns

python

Mapping patterns match on dictionary keys and destructure nested values in one expression. The sequence pattern [*items] captures the list inside the nested dict. Cases are tried top to bottom, so the more specific match (non-empty items) is checked before the empty-list fallback.

Command dispatcher with sequence patterns

python

Sequence patterns destructure the token list by position and length simultaneously. The star pattern (*flags) captures remaining arguments. A guard clause restricts valid environments, while the next case catches the invalid-environment path. This replaces argparse for simple internal CLI tools.

AST node processing with class patterns

python

Class patterns match dataclass instances by type and attribute values at once. The recursive evaluate function handles each node type cleanly. Matching on op="+" inside BinOp narrows both the type and the operation in a single pattern. This is the canonical use case for pattern matching — tree-walking interpreters, compilers, and linters.

Event handling with guard clauses and capture patterns

python

Guard clauses let you add conditions that depend on runtime values like the current time or amount thresholds. The stale-login check combines a class pattern with a timestamp comparison. Notice how captured variables (uid, ip, amt) are immediately available in the case body without any extra unpacking.

Wildcard and complex nested patterns

python

Nested mapping patterns validate configuration structure and types in a single expression. Using str(host) and int(port) inside patterns checks the type and captures the value simultaneously. The wildcard _ at the end is the catch-all. This approach replaces deeply nested key-existence checks and isinstance calls when validating config dicts.

Common Mistakes

Mistake:

Using a bare name where you meant a literal — `case status_code:` captures any value into `status_code` instead of matching the variable's value

Fix:

Bare names are always capture patterns. To match a constant, use a dotted name (`case HTTPStatus.OK:`), a literal (`case 200:`), or a guard clause (`case code if code == expected_status:`). This is the single most common source of bugs with match/case.

Mistake:

Putting a catch-all `case _:` or a broad capture pattern before more specific cases — later cases become unreachable dead code

Fix:

Order cases from most specific to least specific. Python tries patterns top to bottom and takes the first match. Put guarded patterns before unguarded ones for the same structure, and put the wildcard last.

Mistake:

Expecting mapping patterns to require an exact match on all keys — `case {"status": 200}:` matches any dict that *contains* the key `"status"` with value 200, even if it has 50 other keys

Fix:

Mapping patterns match on key presence, not exact shape. This is by design — it lets you match on the keys you care about. If you need to reject extra keys, add a guard: `case {"status": 200, **rest} if not rest:`.

Mistake:

Forgetting that sequence patterns distinguish lists from tuples — `case [x, y]:` matches a list but not a tuple, and `case (x, y):` is grouping, not tuple matching

Fix:

Use `case [x, y]:` for both lists and tuples in practice — Python's sequence pattern matches any sequence type. But `case (x, y):` is treated as a grouping expression, not a tuple pattern. For explicit tuple matching, use `case tuple((x, y)):`.

Best Practices

  • Use pattern matching when branching on data shape — if you're testing isinstance(), checking dict keys, or unpacking tuples to decide what to do, match/case collapses that into a single readable construct
  • Combine class patterns with dataclasses for domain objects: define your data as dataclasses, then match on them by attribute. This gives you destructuring and type narrowing that reads like a specification
  • Keep guard clauses for runtime conditions that patterns can't express — thresholds, date comparisons, regex checks. If you can express it structurally, prefer a pattern over a guard
  • Always include a wildcard `case _:` or a final capture pattern as the last case. Unmatched values silently fall through with no error, which is almost never what you want in production code
  • Avoid deeply nested patterns that span 4+ levels — they're hard to read and brittle. Extract inner structures into helper functions that each do their own match

Summary

Structural Pattern Matching (match/case, Python 3.10+) lets you branch on the shape of data rather than testing values one condition at a time. Literal patterns match exact values, sequence patterns destructure lists, mapping patterns extract dictionary values by key, and class patterns match object attributes — all in a single expression that binds variables automatically. Guard clauses handle runtime conditions that pure structure can't express. Use it for request routing, event dispatching, config validation, AST walking, and anywhere you're currently writing isinstance/elif chains. Always order cases from most specific to least specific, and always end with a wildcard.

Practice Python with hands-on challenges

Learn python pattern matching hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Python with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.