skip to content

Destructuring Syntax

Testing a value against a shape and pulling it apart in one step, through literal, sequence, mapping and class patterns. Interviewers ask because it replaces stacks of isinstance checks.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

22

Why does `case RED:` in a Python match statement rebind RED instead of comparing against it?

level: juniorimportance: must knowfreq 50%

answer

  1. The left of a case is not an expression
  2. Some patterns can never fail
  3. Binding, not comparing
  4. A dot changes the meaning entirely
  5. Colour.RED versus RED

basics

~20 s

A bare name in a case is a capture pattern: it always matches and binds the subject to that name, comparing nothing. To compare against a named constant you must write a dotted value pattern such as Color.RED.

solid answer

~40 s

In the pattern grammar added in Python 3.10 (PEP 634), a pattern is not an expression. A single undotted name other than `_` is a **capture pattern**: it is irrefutable — it matches any subject — and it binds the subject to that name, so `case RED:` overwrites `RED` and fires for everything. Comparing against a named constant requires a **value pattern**, which the grammar defines as a *dotted* name: `case Color.RED:` looks the attribute up at match time and compares it with `==`. The dot is the whole difference. CPython rejects the mistake at compile time whenever the capture is followed by another case, with a name-capture error saying the remaining patterns are unreachable; it stays silent only when the bare-name case is the last one.

code

python · 8 lines
python
RED = "red"
colour = "green"

match colour:
    case RED:
        print("matched, RED is now", RED)

print("module constant:", RED)

go deeper

for a junior

Recall the one-line rule: a bare name after case binds and always matches, a dotted name compares. Be able to spot case RED: in a snippet and say what the branch will actually do.

for a middle

Explain the mechanics: capture patterns are irrefutable, they assign with normal scoping rules, and value patterns do an attribute lookup at match time compared with ==. Know that CPython raises a name-capture SyntaxError when the capture is not the last case.

for a senior

Show the production angle: the silent form is a final bare-name case, so a dispatch table can quietly route everything to one branch. Be ready to describe how you would confirm it and why an enum is the structural fix.

for a principal

Own the convention. Decide that constants used in dispatch live on enums rather than as loose module names, make 'a case name without a dot binds' a review rule, and weigh whether pattern matching earns its keep over dict dispatch in your codebase.

### Patterns are not expressions `match` / `case` arrived in Python **3.10** (PEP 634). The thing after `case` is a **pattern**, and a pattern is a distinct grammatical construct — it is not an expression that gets evaluated and compared. The grammar recognises a handful of pattern kinds: literal patterns, the wildcard `_`, **capture patterns**, **value patterns**, group patterns, and the structural sequence, mapping and class patterns. Capture and value patterns look almost identical on the page, and that near-identity is the single most common `match` bug. ### A bare name is a capture pattern A pattern consisting of one plain name — anything that is not `_` and contains **no dot** — is a capture pattern. It does two things: it **always matches** (it is *irrefutable*, it cannot fail for any subject), and it **binds** the subject to that name using ordinary assignment rules. Inside a function the name becomes a local; at module level it rebinds the global. Nothing is compared. So: ```python RED = "red" colour = "green" match colour: case RED: print(RED) # green -- RED now names the subject ``` The case fires for `"green"`, for `None`, for a list, for anything, and the constant `RED` is gone. ### Why the language was designed this way PEP 635 explains the trade. Destructuring is the *point* of `match`, so binding sub-values has to be the cheap, unmarked spelling — a syntax like `case Point(x=$x)` was rejected as noise. That leaves the question of how to *compare* against a named constant, and the answer is to require a qualifier: a **value pattern** is a dotted name such as `Color.RED`, `Level.WARN` or `limits.MAX`. The dot is what tells the compiler "load this and compare it" rather than "store into this". Making the distinction syntactic — rather than, say, resolving it by whether the name happens to be defined — means the meaning of a case never depends on what else is in scope, and a typo can never silently flip a comparison into a capture that quietly swallows every subject. ### The compiler catches most of the mistake for you An irrefutable pattern makes every later case unreachable, so CPython rejects it at **compile time**: ```python match value: case RED: # SyntaxError: name capture 'RED' makes remaining patterns unreachable ... case GREEN: ... ``` That guard covers the majority of real occurrences — which is why the trap is famous but usually loud. It stays silent in exactly one shape: when the bare-name case is the **last** case in the statement. Then there is nothing left to make unreachable, the module compiles, and the branch simply swallows everything that reached it. The same silence applies to the sole case of a one-case `match`. ### What a value pattern actually does `case Color.RED:` performs the attribute lookup **at match time**, every time that case is tried, and compares the loaded object to the subject with `==`. It never binds anything. Two practical consequences follow: the pattern is not frozen at compile time, so if the attribute is rebound between calls the matching changes with it; and the comparison respects whatever `__eq__` the objects define. For `enum.Enum` members that comparison is identity-based, because `Enum` does not define `__eq__`, which is why enums are the idiomatic home for constants used in `match`. ### The three ways to write the constant * **An enum** — `class Level(enum.Enum): OK = 0; WARN = 1` and then `case Level.WARN:`. Self-documenting, gives you the dot for free, and members compare by identity. * **A module** — `import limits` (not `from limits import MAX`) and then `case limits.MAX:`. * **A namespace object** — constants hung on a small class or on `types.SimpleNamespace`, then `case Thresholds.WARN:`. There is no way to write a bare constant name and have it compare; the grammar has no escape hatch, and a subscript such as `case table["max"]:` is a syntax error too. ### Binding is not conditional on the case winning Bindings made while a pattern is being tried are **not** rolled back when the pattern later fails. A compound pattern can bind its first few names and then fail on a later sub-pattern, leaving those names assigned. That is why you should not read a capture name outside the branch that matched, and why the names bound by a `match` outlive the statement in exactly the way a `for` loop variable does. ### The one name that does not capture `_` is the **wildcard**: it always matches like a capture pattern but binds nothing, which is what makes `case _:` the conventional catch-all. ### How this shows up in an interview The question is usually posed as a small snippet with a module-level constant and a `case` that names it, and the expected answer has three parts: the case is a capture pattern, it always matches and rebinds the name, and the fix is to give the constant a dotted path — normally by making it an `enum.Enum` member. A candidate who adds that CPython refuses to compile the non-final form, with a name-capture error, is showing they have actually hit it.

  • What does CPython do if a bare-name case is followed by more cases?
    It refuses to compile the module. An irrefutable pattern makes everything after it unreachable, so the compiler raises a SyntaxError naming the captured name and saying the remaining patterns are unreachable. That guard is why the trap is usually loud; it goes silent only when the bare-name case is the last case in the statement, or the only one.
  • How do you compare against a module-level constant that has no natural namespace?
    Give it a dot. Either import the module rather than the name — `import limits` then `case limits.MAX:` — or promote the constants to an `enum.Enum` and match on members, or hang them on a small class or a `types.SimpleNamespace`. The value-pattern grammar requires at least one dot, and a subscript such as `case table["max"]:` is a syntax error.
  • Is the dotted name in a value pattern resolved once, or on every match?
    On every attempt. `case limits.MAX:` performs the attribute lookup at match time, each time that case is tried, and compares the result to the subject with `==`. Nothing is frozen at compile time, so rebinding the attribute between calls changes what the case matches — which is another reason to prefer immutable enum members for constants used in a match.

A bare name in a case is a labelled box, not a photograph to compare against: Python drops whatever arrived into the box instead of checking whether it looks like what used to be there.

saying these in an interview costs you the question

  • Says `case RED:` compares the subject to the constant RED
  • Thinks a capture pattern can fail to match some subjects
  • Believes a bare name and the wildcard `_` behave identically
  • Expects a runtime warning instead of a compile-time error
  • Claims quoting or uppercasing the name makes it compare
  • Says the constant must be redefined inside the match

context

open as a page

What does `case _:` match in a Python match statement, and where must it appear?

level: juniorimportance: must knowfreq 55%

basics

~20 s

case _: is the wildcard pattern: it matches any subject and binds nothing, so the name _ keeps whatever value it already had. Because it always succeeds, it must be the last case in the match statement.

open as a page

What does the `if` guard on a `match` statement's `case` clause do?

level: juniorimportance: must knowfreq 42%

basics

~20 s

A guard is an extra if expression written after a case pattern. It is checked only once that pattern has matched and bound its names; a falsy guard rejects the case and matching moves on to the next one.

open as a page

In a Python match statement, what does `case [a, b, *rest]` match, and what does it bind?

level: juniorimportance: must knowfreq 50%

basics

~20 s

It matches any sequence of at least two items - a list, tuple, range, deque and so on, but never a str or bytes. The first two items bind to a and b, and everything left over binds to rest as a new list.

open as a page

Why does Python's match statement need no break at the end of each case?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Python's match statement has no fall-through. The first case whose pattern matches runs its block, and control then jumps past the whole statement, so there is nothing left to break out of.

open as a page

How does `__match_args__` decide what a positional class pattern binds in Python?

level: middleimportance: must knowfreq 45%

basics

~20 s

__match_args__ is a class attribute holding a tuple of attribute names. A positional class pattern maps its sub-patterns onto that tuple in order, so case Job(a, b) reads the first two names. More positional sub-patterns than entries raises TypeError.

open as a page

In Python, what happens when a match statement's subject matches no case?

level: middleimportance: must knowfreq 45%

basics

~20 s

Nothing happens. The whole match statement becomes a no-op: no exception is raised, no default branch runs, and execution continues at the next statement. To make an unmatched subject an error, add case _: and raise there yourself.

open as a page

Why does `case [x, y]` in a Python match statement never match the string "ab"?

level: middleimportance: must knowfreq 45%

basics

~20 s

Pattern matching deliberately excludes str, bytes and bytearray from sequence patterns, even though they are sequences everywhere else in Python. Silently destructuring text into characters is almost always a bug, so the pattern just fails and the next case is tried.

open as a page

What does the class pattern `case Point(x=0)` test in a Python match statement?

level: juniorimportance: should knowfreq 38%

basics

~10 s

It runs isinstance(subject, Point) first, then reads the subject's x attribute and matches it against the sub-pattern 0. Despite the call syntax, nothing is constructed and Point.__init__ never runs.

open as a page

In a Python match statement, how does a literal pattern compare the subject to the case value?

level: middleimportance: should knowfreq 35%

basics

~10 s

Literal patterns compare with ==, except None, True and False, which are compared with is. So case 1: also matches True and 1.0, while case True: matches only the True object.

open as a page

Why must every alternative of a `match` or-pattern bind the same names?

level: middleimportance: should knowfreq 34%

basics

~20 s

Because the case body must be able to read every captured name no matter which alternative matched, and Python has no maybe-unbound state. The compiler enforces it, raising SyntaxError: alternative patterns bind different names before the code ever runs.

open as a page

In a Python match statement, what does the mapping pattern `case {"kind": k}` require of the subject?

level: middleimportance: should knowfreq 38%

basics

~20 s

Only that the subject is a mapping which contains the key "kind"; its value binds to k. Extra keys are ignored and key order is irrelevant, so case {} matches any mapping at all rather than only an empty one.

open as a page

In a Python match statement, how often is the subject expression evaluated?

level: middleimportance: should knowfreq 45%

basics

~20 s

Exactly once. Python evaluates the expression written after the match keyword before any case is tried, then tests that one value against each pattern in turn, so an expensive or side-effecting call runs a single time.

open as a page

Why can reordering a dataclass's fields silently change what `case Batch(a, b)` binds?

level: seniorimportance: should knowfreq 24%

basics

~10 s

dataclasses.dataclass regenerates __match_args__ from the new field order, so positional class patterns elsewhere still compile, still match, and now bind different attributes to the same names. Nothing raises, because the arity is unchanged.

open as a page

Why does Python never tell you a match statement is missing a case?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Because Python is dynamically typed and its patterns resolve at runtime: value patterns are name lookups, class patterns admit any subclass, and guards are arbitrary expressions. There is no closed set of cases to check against.

open as a page

Why does a `match` guard's helper call run twice per record in a geocoding batch?

level: seniorimportance: should knowfreq 26%

basics

~20 s

Because guards are evaluated per case, in source order, until one succeeds. Two overlapping cases that both call the lookup in their guards call it twice: the first case's guard ran and paid its side effect before rejecting the record.

open as a page

Why does `case str(s)` bind the whole string instead of an attribute in Python?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

str is one of a dozen builtin types the match statement special-cases: a single positional sub-pattern matches the whole subject rather than an attribute, so case str(s) means 'is a str, and bind it to s'.

open as a page

What does an `as` pattern such as `case [x, y] as pair` bind in a `match`?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

An as pattern binds the name on its right to the value its left-hand pattern matched, on top of any names that pattern captured. At the top level that value is the whole subject; nested, it is only the sub-value.

open as a page

Why are match and case usable as ordinary variable names in Python?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

They are soft keywords: reserved only in the grammatical positions where a match statement can appear. Everywhere else the parser treats them as ordinary identifiers, so upgrading to Python 3.10 broke no code that already used those names.

open as a page

A match statement in a telemetry collector routes every reading to its final case — how do you diagnose it?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

A branch that fires for every input is a capture pattern: the final case names a constant without a dot, so it matches anything and rebinds the name. Adding a case after it makes the module refuse to compile.

open as a page

How do you make a custom container class match a sequence pattern in a Python match statement?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Defining __len__ and __getitem__ is not enough. The class must inherit from collections.abc.Sequence or be registered with it, because pattern matching checks an internal type flag set only by that inheritance or registration - it does not duck-type.

open as a page

Why does a module containing a match statement fail to import on Python 3.9 even if that branch never runs?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

Python compiles a whole module before running any of it, so syntax an older interpreter does not know is a compile-time SyntaxError. A runtime version check comes too late; the match statement must live in a separately imported module.

open as a page