Articles
Python Structural Pattern Matching Interview Questions: Captures, Guards, and Classes
Practice Python match captures, guards, mappings, sequences, and class patterns with executable examples and a concrete message-acceptance matrix.
In a Python structural pattern matching interview, case expected: is often the trap. It looks as though it compares the subject with an existing variable. Instead, a bare name captures the subject. If you meant to accept only status 200, you may have written a branch that also accepts 404 and rebinds your supposed constant.
Explain a match in three stages: shape, bindings, then guard. Identify what structure the pattern accepts, which names it creates, and which condition decides whether that case executes. This gives you a precise answer when an interviewer adds a missing field, changes a class attribute, or asks why a fallback never runs. Practice the wider decoding problem with Build message schema parser with size/type queries.
Evidence boundary: Official rules come from Python's current 3.14 reference, PEP 634, and PEP 636. The examples, acceptance matrix, and interview follow-ups are original material checked on CPython 3.12.14. They are not candidate reports or employer-specific interview claims. Structural pattern matching requires Python 3.10 or later; no static exhaustiveness guarantee is assumed.
Does a bare name compare or capture?
The language reference defines a bare name in a capture pattern as a binding, whereas a dotted name can be a value pattern. _ is a wildcard and binds no name. These are syntax categories; capitalizing a variable does not turn a bare name into a constant comparison. Python compound statements reference.
Run this original fixture:
def classify(status):
expected = 200
match status:
case expected:
return expected
assert classify(200) == 200
assert classify(404) == 404
Both calls match. The second result is a newly captured value, not proof that the original value 200 equals 404. Adding an unguarded fallback after that capture produces a compile-time error because the capture is irrefutable and makes the later case unreachable.
A correct constant comparison can use a literal or a qualified value:
from http import HTTPStatus
def accepted(status):
match status:
case HTTPStatus.OK:
return True
case _:
return False
assert accepted(200)
assert not accepted(404)
The dotted pattern performs a value comparison. It is not a runtime check that the subject itself is an HTTPStatus member. If the product requires exactly that enum representation, state that additional requirement separately. An interview answer should distinguish “compares equal to this value” from “has exactly this type.”
Our recommended debugging step is to read each pattern aloud. “Capture into expected” exposes the bug faster than “match expected,” which preserves the ambiguity that caused it.
When does the guard run?
PEP 634 specifies that a guard is evaluated after its pattern succeeds. A false guard allows later cases to be considered; a guard exception propagates. Successful pattern bindings are available to the guard. Do not rely on the persistence or absence of partial bindings from a pattern that ultimately fails: the specification deliberately leaves that behavior open. PEP 634.
Consider this original price example:
def price_band(message):
match message:
case {"kind": "price", "amount": amount} if amount > 0:
return "positive"
case {"kind": "price", "amount": _}:
return "nonpositive"
case _:
return "other"
For {"kind": "price", "amount": 0}, the first pattern succeeds but its guard is false. The second case handles the input. For the same shape with "amount": "free", the comparison can raise TypeError; Python does not silently treat that exception as a failed guard.
That makes the second branch's label misleading for arbitrary external data. It is only nonpositive under an already-established numeric contract. A better decoder checks the permitted scalar representation or rejects invalid data before calculating the price band.
Guards should express a business predicate whose inputs are already understood. A guard that also updates a database or increments a persistent counter makes branch selection difficult to reason about. This is a design recommendation rather than a language restriction: guards are expressions and can call code with side effects.
For tests, separate three outcomes: shape mismatch, valid shape with a false guard, and an exception during guard evaluation. A single happy-path assertion does not distinguish them.
Does a mapping pattern require exactly those keys?
No. PEP 636 illustrates mapping patterns that select required entries while permitting additional entries unless the application imposes a narrower contract. The pattern is useful for extracting a structured message; it is not automatically an exact schema validator. PEP 636 tutorial.
Here is an original decoder with an explicit Boolean exclusion for an integer amount:
def decode(message):
match message:
case {"kind": "price", "amount": int() as amount} if type(amount) is int and amount > 0:
return ("price", amount)
case {"kind": "cancel", "id": str() as item_id} if item_id:
return ("cancel", item_id)
case _:
raise ValueError("unsupported message")
The int() subpattern accepts integer instances, including Boolean instances through subclassing. This original contract deliberately uses type(amount) is int in the guard to reject True. A different application might accept additional numeric types; define that intentionally rather than copying this exact policy everywhere.
Use this acceptance matrix:
| Original input | Expected result | Reason to explain |
|---|---|---|
{"kind": "price", "amount": 5} | ("price", 5) | Required shape and positive exact integer. |
Same input plus "trace": "t1" | Accepted | Extra mapping entries are permitted here. |
{"kind": "price"} | Rejected | Required amount is absent. |
{"kind": "price", "amount": True} | Rejected | The explicit guard excludes Boolean values. |
{"kind": "price", "amount": "5"} | Rejected | String input does not satisfy the integer subpattern. |
{"kind": "cancel", "id": ""} | Rejected | Shape matches, but the nonempty-ID guard fails. |
If unknown keys must be rejected, capture remaining keys with **rest and require the remainder to be empty, or validate through an appropriate schema boundary. State why exactness matters. A forward-compatible event consumer may intentionally tolerate metadata added by a newer producer.
A missing mapping key does not become present merely because a defaultdict would manufacture it during ordinary subscription. Pattern matching uses mapping lookup semantics designed not to invoke that missing-key construction path. Add that case to the fixture if your application relies on default mappings.
Do sequence patterns accept every iterable?
No. A sequence pattern is not a general loop over arbitrary iterable input. Strings, bytes, and bytearrays are excluded from sequence-pattern matching. A generator also does not become a sequence merely because it can produce elements. These distinctions are specified in PEP 634.
For an original command decoder, case ["move", x, y]: can accept a suitable list or tuple with three elements. It does not accept the text "move" as a sequence of characters, nor does it automatically consume a generator to search for the right shape.
Ask whether the API accepts a concrete sequence or a stream. If it accepts a stream, parsing and buffering rules belong before structural matching. Converting every iterable to a list may be acceptable for a bounded tiny request but disastrous for an unbounded source. The pattern does not remove that resource decision.
A starred capture such as case [head, *tail]: gives the remaining matched elements to tail. Define maximum input size and whether you need that materialized remainder. For a large payload, an explicit parser with bounds may communicate the contract more clearly.
Test an empty sequence, the expected length, one extra element, a string, and a generator. Explain each result from the accepted structure instead of treating unexpected nonmatches as implementation bugs.
Does Point(x, y) call the constructor?
A class pattern checks an existing subject and matches attributes. It does not construct a new Point from the pattern arguments. Positional class patterns use __match_args__ to identify attribute names; keyword patterns identify attributes directly. The tutorial demonstrates how dataclasses support this style.
Use an original dataclass fixture:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def location(subject):
match subject:
case Point(0, y):
return ("vertical", y)
case Point(x=x, y=y) if x == y:
return ("diagonal", x)
case _:
return ("other", None)
assert location(Point(0, 8)) == ("vertical", 8)
assert location(Point(3, 3)) == ("diagonal", 3)
The first case wins for Point(0, 0) too. Case order is an explicit priority, so diagonal membership does not override the earlier vertical branch. The int annotations on the dataclass do not themselves enforce runtime field values.
Class patterns can match subclass instances, and attribute access can execute properties. An AttributeError during the specified attribute lookup causes that class pattern to fail; other exceptions can propagate. Therefore a pattern against a framework object is not necessarily a side-effect-free inspection of raw stored fields.
Our recommendation is to prefer keyword class patterns when the attribute names are central to clarity or positional order might change. If the public API promises positional matching, changing __match_args__ is an observable compatibility change. Test that contract rather than relying only on constructor tests.
What errors should you catch before running the program?
Some malformed patterns are syntax errors. For example, OR alternatives must bind the same set of names. An original attempt such as case {"x": x} | {"y": y}: cannot provide one coherent binding contract to the case body.
Repair it by binding the same name in both alternatives, if their meanings truly fit one branch: case {"x": value} | {"y": value}:. If the fields represent different business concepts, separate the cases rather than forcing a syntactic repair that erases useful distinctions.
Duplicate captures in one pattern are another error. case [x, x]: does not mean “two equal elements.” Capture two names and use a guard, such as case [left, right] if left == right:. Specify the equality behavior expected for those objects.
Python does not guarantee that a match covers every value in your domain. An explicit fallback can reject unsupported messages; silently doing nothing after no match may leave a caller without a meaningful response. Decide whether fallback means ignore, reject, or preserve compatibility.
For interview preparation, keep a compact executable file containing accepted inputs, rejected inputs, guard exceptions, and snippets checked with compile() for expected syntax errors. The file should prove behavior, not merely repeat the implementation's branch names.
Sources and Further Reading
- Python 3.14 compound statements and match reference
- PEP 634: Structural pattern matching specification
- PEP 636: Structural pattern matching tutorial
Sources checked October 1, 2026. Exercises were checked on CPython 3.12.14; avoid relying on unspecified partial bindings from failed patterns.