skip to content

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

level: seniorimportance: nice to knowfreq 18%

answer

  1. Duck typing does not get you in here
  2. The check is one cheap type test
  3. An abstract base class decides it
  4. There are two ways to opt in

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.

solid answer

~40 s

Sequence patterns do not probe for methods. The interpreter tests a flag on the subject's type, and that flag is set only for classes that inherit from `collections.abc.Sequence` or are passed to its `register()`. A class defining `__len__` and `__getitem__` alone therefore falls straight through every bracketed `case`, which is a genuinely confusing failure because the class works with `len`, indexing, slicing and iteration. Subclassing the ABC is the cleaner route since it also gets you `__contains__`, `__iter__`, `index` and `count` for free; `register()` is the escape hatch when you cannot change the base classes, and it propagates to existing subclasses. The same rule holds one door over: a custom mapping must inherit from or be registered with `collections.abc.Mapping` before a brace pattern will look at it.

code

python · 20 lines
python
from collections.abc import Sequence

class Batch:
    def __init__(self, items):
        self._items = list(items)
    def __len__(self):
        return len(self._items)
    def __getitem__(self, index):
        return self._items[index]

def head(value):
    match value:
        case [first, *_]:
            return first
        case _:
            return None

print(head(Batch([17, 18])))
Sequence.register(Batch)
print(head(Batch([17, 18])))

go deeper

for a junior

Know the headline only: your own class does not automatically match a bracketed case just because it supports len and indexing - it has to be declared a sequence via collections.abc.Sequence.

for a middle

Explain both opt-in routes and their difference: inheriting from the ABC also brings mixin methods and catches an incomplete implementation, while register() is an unchecked assertion useful for classes you cannot re-base.

for a senior

Show how you would diagnose the silent fall-through in a running system - an isinstance check against the ABC at the failure point - and why the symptom looks like bad data rather than a typing mistake.

for a principal

Own the consistency call: whether your domain containers subclass the collection ABCs as a house rule, given that late registration makes match behaviour depend on import order while inheritance settles it at class definition.

Almost everything in Python is **structural**: define `__len__` and `__getitem__` and your object indexes, slices and iterates like a list, and no declaration is required. Structural pattern matching, despite the name, is the conspicuous exception. A sequence pattern asks a **nominal** question - *is this type declared to be a sequence?* - and gets its answer from a flag on the type object rather than from the methods present. ## Why it works that way A `match` statement may run a long ladder of cases against every subject, so the first test in each case has to be extremely cheap. Probing for `__len__` and `__getitem__` on each attempt would cost attribute lookups, and worse, it would be wrong: `str` defines both and must not match, while a mapping defines both and means something entirely different by them. A single bit on the type: - is one comparison, - is inherited by subclasses at class-creation time, - and lets the language draw exactly the distinctions it wants - sequences in, text out, mappings routed to their own pattern form. ## The two ways to opt in 1. **Inheriting from `collections.abc.Sequence`** is the primary route. You implement `__len__` and `__getitem__`, and the ABC contributes `__contains__`, `__iter__`, `__reversed__`, `index` and `count` as mixin methods, so you get a fuller sequence for less code and the flag comes with it. 2. The alternative is **registration** - passing your class to `collections.abc.Sequence.register()` - which asserts conformance without changing the class's bases. Registration is the tool for a class you do not own or one whose metaclass or layout you cannot disturb, and it propagates: subclasses created before or after the call match too. The corresponding mapping story is identical with `collections.abc.Mapping` in place of `Sequence`. ## The failure it prevents you from noticing The symptom is **a case that never fires**. Your class supports `len`, `for`, indexing and slicing; unpacking with `first, *rest = batch` works perfectly; and then a bracketed `case` silently skips it and control lands in the catch-all. There is no error and no warning, because a pattern that does not apply is not an error - falling through is exactly what patterns do. Consider a job that fans a nightly digest across a seventeen-service dependency graph: each service hands back a small result container, and the dispatcher matches those containers by shape. - If one team's container inherits from the ABC and another team's merely implements the two dunder methods, half the graph routes correctly and half falls into the default arm. - Nothing crashes; the digest is quietly incomplete, and the bug looks like a data problem rather than a typing one. The five-second diagnosis is to check `isinstance(obj, collections.abc.Sequence)` at the point of failure - if that is `False`, you have your answer. ## Registration is a promise, not a check `register()` performs no verification. Register a class with no `__getitem__` at all and it will start matching sequence patterns, then raise as the pattern tries to index it. The ABC machinery trusts you, which is the price of letting registration work on classes it cannot inspect meaningfully. - Prefer inheritance where you have the choice, - and where you register, make the conformance visible - a test that matches an instance against a representative pattern is cheap and it pins the behaviour down. ## The design judgement Because the flag is nominal, a codebase that wants its own containers usable in pattern-matching dispatch has to decide that up front. Retrofitting is easy in mechanism and awkward in practice: - adding a base class late can disturb a metaclass or an `__slots__` layout, - and adding registration late means the behaviour of existing `match` statements changes depending on whether the registering module has been imported yet. That **import-order sensitivity** is the real argument for inheritance: it is a property of the class, established at definition, rather than a side effect of somebody's module having run. ## Where the rule stops Finally, keep the boundary of the rule clear. It governs whether your type reaches a sequence pattern at all. Once it does, everything else behaves precisely as it would for a built-in list: - exact arity, - one starred name, - nested sub-patterns, - the starred remainder arriving as a new `list`.

  • Does `collections.abc.Sequence.register()` verify that the class implements the protocol?
    No. Registration is an unchecked assertion of conformance: the class starts matching sequence patterns immediately, and if it lacks `__getitem__` the failure surfaces later, as the pattern tries to index the subject. Inheritance is safer where you can use it, since the ABC then supplies the mixin methods and an abstract-method check catches an incomplete implementation at instantiation time.
  • What is the equivalent requirement for a custom class to match a mapping pattern?
    The same rule with `collections.abc.Mapping`: inherit from it or register with it. Implementing `__getitem__`, `__iter__` and `__len__` alone is not enough for a brace pattern to consider the object, exactly as with sequences. Inheriting is usually easier because the ABC supplies `get`, `keys`, `items`, `values`, `__contains__` and `__eq__` on top of the three you write.
  • How would you diagnose a bracketed `case` that never fires for your own container type?
    Check `isinstance(obj, collections.abc.Sequence)` at the point where the match fails. If it is `False`, the type never opted in and no pattern detail matters. If it is `True`, the type test passed and the problem is arity or a sub-pattern instead - narrow it by matching against `case [*_]`, which accepts any sequence of any length.

It is membership by declared affiliation rather than by behaviour: acting exactly like a member is not enough, you have to have joined.

saying these in an interview costs you the question

  • Claims `__len__` plus `__getitem__` is sufficient to match
  • Says pattern matching duck-types like the rest of Python
  • Believes only built-in types can match a sequence pattern
  • Thinks `register()` validates the class it is given
  • Confuses the sequence opt-in with defining `__match_args__`

context