Python · Python 3.10 and 3.11

Python 3.10: Structural Pattern Matching

match and case - capture versus value patterns, sequence, mapping and class patterns, guards - and the rest of 3.10: union types, zip(strict=True) and better error messages.

  • 5 min read
  • 11 practice questions
On this page
  1. Description
  2. match and case
  3. The rest of 3.10
  4. Which tool for which job?
  5. Common mistakes
  6. Check yourself
  7. Key takeaways
  8. Quick revise
  9. Practice

Description

Python 3.10 added match, the biggest syntax change in years, plus a few smaller tools: X | Y union types, zip(strict=True), parenthesized context managers and much better error messages. match looks like a switch statement from Java or C, and that resemblance causes most of the bugs - it's really structural pattern matching, borrowed from functional languages: it reads the shape of data and pulls pieces out of it.

Here's the kind of bug we mean. A team ports a Java switch on an enum:

RED, GREEN = Color.RED, Color.GREEN

match light:
    case RED:
        stop()
    case GREEN:
        go()

It doesn't compile: SyntaxError: name capture 'RED' makes remaining patterns unreachable. In a pattern, a bare name isn't a value to compare against - it's a variable to bind, so case RED: matches anything and assigns it to RED. (Had it been the only case, it would have silently matched everything.)

We'll go from simple to more technical:

  1. match and case: first match wins, and no match is silent.
  2. Capture patterns versus value patterns - the bug above.
  3. Sequence, mapping and class patterns, and guards.
  4. The rest of 3.10: X | Y, zip(strict=True), better errors.

match and case

match command:
    case ["go", direction]:                       # a 2-item list starting with "go"
        move(direction)
    case ["drop", *items]:                        # "drop" followed by any number of items
        drop_all(items)
    case {"type": "click", "x": x, "y": y}:       # a dict with at least these keys
        click(x, y)
    case Point(x=0, y=y):                         # a Point whose x is 0
        on_y_axis(y)
    case "quit" | "exit":                         # literals, with alternatives
        quit()
    case _:                                       # anything else
        unknown(command)

The rules:

  • First match wins; there's no fall-through and no break. If no case matches, nothing happens - no exception, no exhaustiveness check (a test's match on an enum given the string "red" matched nothing and fell through to the code after it). Add case _: to handle the rest explicitly, or raise in it.
  • Capture vs value patterns. A bare name (direction, x) captures; a dotted name (Color.RED, http.HTTPStatus.OK) or a literal ("quit", 404, None) compares with == (is for None, True, False). That's why constants in patterns must be qualified. _ matches without binding.
  • Sequence patterns match lists, tuples and other sequences - but never str, bytes or bytearray: case [a, b] doesn't match "ab" (a test fell through to case str():), while ("a", "b") and ["a", "b"] both match.
  • Mapping patterns match any mapping with at least the listed keys - extra keys are ignored (a click event with an "extra" key still matched). Use **rest to capture the others.
  • Class patterns check isinstance, then attributes: Point(x=0, y=y) matches a Point whose x == 0 and binds y. Positional sub-patterns (Point(0, 0)) use the class's __match_args__ - dataclasses and named tuples generate it (('x', 'y')). Builtins like str() and int() match by type: case str():.
  • Guards run after a pattern matches and its names are bound: case Point(x, y) if x == y:.

Use match when the shape of data drives the logic - parsing commands, JSON events, ASTs, protocol messages. For a plain value lookup, a dict is often clearer.

The rest of 3.10

  • X | Y unions (PEP 604): def find(id: int) -> User | None, and also at run time - isinstance(5, int | str) is True. Optional[int] is just int | None.
  • zip(..., strict=True): plain zip stops at the shortest input and silently drops the rest ([(1, 'a'), (2, 'b')] from three numbers and two letters); with strict=True it raises ValueError: zip() argument 2 is shorter than argument 1. Use it whenever the inputs must line up.
  • Parenthesized context managers: with (open(a) as f, open(b) as g): across several lines.
  • Better error messages: unclosed brackets reported where they open, Did you mean ...? suggestions for misspelled names and attributes, and precise syntax-error ranges - the start of the "nicer errors" work that 3.11 continued.

Which tool for which job?

You want toUse
Branch on the shape of data (lists, dicts, objects)match with sequence, mapping and class patterns
Compare against a constanta dotted name (Color.RED) or a literal in case
Map a value to a resulta dict lookup
Handle "anything else"case _: (raise in it if nothing else is valid)
An extra condition on a patterna guard: case Point(x, y) if x == y:
Say "an int or nothing" in a type hintint | None
Pair up lists that must be the same lengthzip(a, b, strict=True)

Common mistakes

MistakeWhat happensDo this instead
case RED: with a module constantcaptures everything (or a SyntaxError)case Color.RED:
No case _:unmatched values are silently ignoredadd case _: and raise or handle
case [a, b]: expecting to match "ab"strings never match sequence patternscase str():
Expecting a mapping pattern to require exactly the keysextra keys still matchcapture **rest and check it
case Plain(1): on a class without __match_args__TypeError at run timekeyword patterns, or a dataclass
Plain zip on inputs that must line upthe extras are silently droppedzip(..., strict=True)

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Why is case RED: (with RED a module-level constant) a bug?

Show answer

A bare name in a pattern captures: it matches anything and binds it to RED. Use a dotted name (Color.RED) or a literal to compare.

2. Does case {"type": "click"}: match {"type": "click", "x": 4}?

Show answer

Yes - mapping patterns require at least the listed keys and ignore the rest.

3. What does a match do when no case matches?

Show answer

Nothing - execution continues after it. There's no error and no exhaustiveness check at run time; add case _: to handle or reject the rest.

Key takeaways

  • match destructures shapes; first match wins; no match is silent.
  • Bare names capture - qualify constants.
  • Sequences exclude strings; mappings ignore extra keys; class patterns use __match_args__.
  • 3.10 also brought X | Y, zip(strict=True) and better errors.
  • A dict lookup is still the clearest way to map values to results.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You want toUse
Destructure lists, dicts or objectsmatch with sequence, mapping and class patterns
Compare with a constantcase Color.RED: or a literal
Map values to resultsa dict of handlers
Reject anything unexpectedcase _: raise ...
Exhaustiveness over an enum or unioncase _: assert_never(x) + a type checker
"An int or nothing"int | None
Lists that must line upzip(a, b, strict=True)

Snippets worth knowing by heart

match event:
    case {"type": "click", "x": x, "y": y}:
        click(x, y)
    case ["drop", *items]:
        drop_all(items)
    case Point(x=0, y=y):
        on_y_axis(y)
    case bool():
        toggle(event)
    case "quit" | "exit":
        quit()
    case _:
        raise ValueError(f"unknown event: {event!r}")

for user_id, name in zip(ids, names, strict=True):
    save(user_id, name)

The traps

  • case RED: - a bare name captures; use Color.RED.
  • No match is silent - nothing raises without case _:.
  • Strings never match sequence patterns; mapping patterns ignore extra keys.
  • Positional class patterns need __match_args__ (dataclasses generate it).
  • Plain zip silently drops the extras.

Staff-level points, one line each

  • Compiled to checks tried in order (MATCH_SEQUENCE, MATCH_MAPPING, MATCH_KEYS, MATCH_CLASS) - the 20th literal case took about four times as long as the first.
  • Sequences: any registered Sequence (deque, range, memoryview), never str/bytes/bytearray, never iterators or generators.
  • Mappings: keys are read with get - a defaultdict doesn't match a missing key and gains no key.
  • Literals compare with == (None/True/False with is): True matches case 1:; int() matches True - put bool first.
  • case int(n): binds the whole subject for builtin types.
  • Names bound by a failed case or guard stay bound.
  • Alternatives must bind the same names; an irrefutable case must be last.
  • match/case are soft keywords - match = 5 still works.

Say it in an interview

  • "I'd use match here because it checks the shape and pulls out the fields in one step."
  • "Constants in patterns have to be dotted names, or they capture."
  • "There's a case _ that raises, so an unexpected message fails loudly."
  • "The type checker makes the enum match exhaustive through assert_never."

Practice

This skill comes with 11 multiple-choice questions that tell you which misconception a wrong answer reveals. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Python 3.10: Structural Pattern Matching free

Practice Python 3.10: Structural Pattern Matching free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime