skip to content

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

level: juniorimportance: should knowfreq 38%

answer

  1. The call syntax constructs nothing
  2. Two stages, in a fixed order
  3. A type test, then attribute reads
  4. isinstance first, then getattr
  5. A missing attribute fails quietly

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.

solid answer

~40 s

A class pattern is a type test plus attribute destructuring, in that order. `case Point(x=0)` checks `isinstance(subject, Point)`; only then does it look up `subject.x` and match the sub-pattern `0` against it, comparing with `==`. The call-like syntax borrows the constructor's spelling but calls nothing — no `__init__`, no instance creation. Keyword sub-patterns name attributes directly and work on any class; positional sub-patterns need the class to declare `__match_args__`. Because the first stage is `isinstance`, an instance of a subclass matches a base-class pattern, so more specific cases must be written above more general ones. If the named attribute does not exist, the case simply fails and control moves to the next one — no `AttributeError` escapes. A bare `case Point():` is a pure type test.

code

python · 26 lines
python
class ShardJob:
    def __init__(self, shard, docs):
        self.shard = shard
        self.docs = docs


def route(subject):
    match subject:
        case ShardJob(shard=0, docs=n):
            return f"primary shard, {n} docs"
        case ShardJob(shard=s):
            return f"replica shard {s}"
        case _:
            return "not a shard job"


print(route(ShardJob(0, 1200)))
print(route(ShardJob(4, 900)))
print(route("rebuild"))

try:
    match ShardJob(1, 10):
        case ShardJob(s, n):
            pass
except TypeError as exc:
    print(exc)

go deeper

for a junior

Be ready to read a class pattern aloud correctly: it is an isinstance test followed by attribute reads, not a constructor call. Knowing that case Point(x=0) never builds a Point is the whole ask at this level.

for a middle

Explain the two stages in order and say which sub-pattern form needs __match_args__ and which does not. Interviewers expect you to know that a missing attribute fails the case instead of raising.

for a senior

Demonstrate that you have designed with it: subclass instances match base-class patterns, so case order encodes your type hierarchy, and matching on a property means running that property's code during dispatch.

for a principal

Own the question of whether a class-pattern dispatch chain is the right structure at all, versus polymorphic methods or a registry. Class patterns centralise behaviour outside the classes and re-open every time a new type appears.

A class pattern is written with call syntax — `case ShardJob(shard=0, docs=n):` — and that syntax is the single largest source of confusion about it, because nothing is called. Read it as two stages that run in a fixed order. ## Stage one: the type test The name before the parentheses is resolved as an ordinary expression (it may be dotted, such as `events.ShardJob`) and must evaluate to a class. If it evaluates to anything else — an instance, an integer, a function — the interpreter raises `TypeError` at match time saying the called match pattern must be a class. Given a class, the first thing the pattern does is `isinstance(subject, ShardJob)`. If that is false the case fails immediately and no attribute is touched. Two consequences fall straight out of `isinstance`. First, subclasses match: an instance of a subclass of `ShardJob` matches `case ShardJob():`, so a chain of cases must put the specific classes above the general ones or the general case will swallow them. Second, `typing.Protocol` classes only work in a class pattern when they are decorated with `@typing.runtime_checkable`; otherwise the isinstance check itself raises `TypeError`, and even when it is allowed it only checks that the named attributes exist, not their types. ## Stage two: attributes After the type test, each sub-pattern is matched against an attribute of the subject. *Keyword sub-patterns* name the attribute explicitly: `shard=0` means "read `subject.shard`, then match the pattern `0` against it". The sub-pattern `0` is a literal pattern, so the comparison is `subject.shard == 0`. Keyword sub-patterns work on any class at all — a plain class with a hand-written `__init__`, a class whose attributes are properties, a class whose attributes appear only at runtime. *Positional sub-patterns* — `case ShardJob(s, n)` — have no attribute names of their own, so the class must supply them through `__match_args__`, a tuple of attribute names. A class with no `__match_args__` raises `TypeError` the moment a positional class pattern is tried against one of its instances. `dataclasses.dataclass` generates that tuple for you, which is why positional class patterns feel effortless on dataclasses and fail on ordinary classes. A pattern with no sub-patterns at all, `case ShardJob():`, is a pure type test — the pattern-matching spelling of an isinstance branch, and usually the cleanest way to dispatch over a closed set of message or event classes. ## What happens when the attribute is missing Nothing dramatic. The language reference defines the keyword step as `hasattr(subject, "shard")` followed by matching the sub-pattern, so a case naming an attribute the subject does not have simply does not match, and control passes to the next case. This is deliberate: patterns are tests, and a failed test is not an error. Do not write `try`/`except AttributeError` around a `match` expecting to catch it. ## Attribute access is real attribute access The lookups go through the normal descriptor machinery. A `property` is evaluated, a `__getattr__` fallback fires, a computed attribute does its computation. CPython gathers every attribute the pattern names before testing any sub-pattern, so those side effects run even when the first sub-pattern would have failed the case. Keep expensive or side-effecting properties out of the attributes you match on. One more subtlety worth knowing early: names inside a pattern may already be bound by the time a later sub-pattern fails, so a value captured by a case that ultimately did not match must not be relied on afterwards. Only read the names bound by the case whose body is executing. ## What it is not It is not a constructor call: `__init__` does not run and no object is created. It is not an equality test against an instance: `case Point(x=0)` does not build a `Point` and compare it with `==`; only the sub-patterns you write do any comparing. And it is not duck typing — the `isinstance` gate is real, so a different class carrying identical attributes will not match. The practical shape this takes in real code is a dispatch chain: a handler function that matches an incoming object against a handful of classes, pulling out one or two fields per case with keyword sub-patterns, and ending with a catch-all. Written that way, a class pattern reads as "is it this kind of thing, and does the field I care about look like this?" — which is exactly what it executes.

  • Does a class pattern match an instance of a subclass?
    Yes. The first stage is `isinstance`, so any subclass instance matches a base-class pattern. That makes case order load-bearing: put the specific classes above their base class, or the base-class case will absorb them all. If you need an exact-type test you have to write it yourself, typically by matching the base class and comparing `type(subject)` in a guard.
  • What happens if the subject has no attribute the pattern names?
    The case fails and the interpreter moves to the next one; no `AttributeError` escapes the `match`. The reference defines the keyword step as a `hasattr` check followed by matching the sub-pattern, so patterns behave as tests rather than as accesses. An exception raised inside a property you are matching on is a different matter — that does propagate.
  • Can you use a `typing.Protocol` class in a class pattern?
    Only if it is decorated with `@typing.runtime_checkable`; otherwise the isinstance check raises `TypeError`. Even then the check is structural in name only — it verifies the attributes exist, not their types — so `case MyProto():` is a weaker gate than it looks, and any real validation has to come from the sub-patterns you write or a guard.

It reads like a constructor call but behaves like a customs check: first confirm the passport says the right class, then open exactly the two pockets you named.

saying these in an interview costs you the question

  • Says the pattern calls the class constructor
  • Thinks the subject is compared to a new instance with ==
  • Believes a missing attribute raises AttributeError
  • Expects exact-type matching rather than isinstance
  • Assumes positional sub-patterns work on any class
  • Calls it duck typing with no type check

context