skip to content

Why is isinstance(obj, collections.abc.Sequence) False for a class with __len__ and __getitem__?

level: seniorimportance: should knowfreq 35%

answer

  1. isinstance means something different for ABCs
  2. Some abstract classes check structurally, some do not
  3. One method implies its own contract
  4. __getitem__ cannot distinguish two shapes
  5. register() promises without proving

basics

~20 s

Only the single-method ABCs — Iterable, Container, Sized, Hashable, Iterator, Reversible, Collection — check structurally for the methods they name. Sequence and Mapping define no such hook, so the check passes only for real subclasses or explicitly registered classes.

solid answer

~40 s

`isinstance` against a `collections.abc` class goes through `ABCMeta`, which consults the ABC's `__subclasshook__`, then its registry of virtual subclasses, then the real class hierarchy. The one-method ABCs implement a hook that simply looks for the named method, so `Sized` and `Container` accept any class with `__len__` or `__contains__`. `Sequence` and `Mapping` deliberately implement none, because `__getitem__` cannot tell them apart — the same method name means integer positions in one and arbitrary keys in the other — and because ordering, `index`, `count` and `keys()` are semantics no method-name check can verify. So you have three options: inherit from the ABC and gain its mixin methods, call its `register()` classmethod to make the check pass without gaining or proving anything, or skip the check and just use the object. In application code, prefer the last.

code

python · 16 lines
python
import collections.abc

class FlagSet:
    def __init__(self, names):
        self._names = list(names)

    def __len__(self):
        return len(self._names)

    def __getitem__(self, index):
        return self._names[index]

flags = FlagSet(["dark_mode", "beta_checkout"])
print(isinstance(flags, collections.abc.Sized),
      isinstance(flags, collections.abc.Iterable),
      isinstance(flags, collections.abc.Sequence))

go deeper

for a junior

Take away the headline: these abstract classes are not all the same. Some accept any object that has the right method, and Sequence and Mapping are not among them — a class must inherit from them or be registered.

for a middle

Explain the machinery: ABCMeta runs a structural hook where one exists, then a registry of virtual subclasses, then real inheritance. Say why getitem alone cannot distinguish a sequence from a mapping.

for a senior

Show judgement about when to check at all. Name registration as an unverified promise, prefer the ABC over a concrete type when you do branch, and raise the str-is-a-Sequence trap unprompted — it is a real production bug, not trivia.

for a principal

Own the policy: where type checks belong in a codebase at all, whether public APIs should advertise ABCs or protocols, and the cost of registering types you do not control so that other people's checks start passing on your behalf.

## What `isinstance` does when the second argument is an ABC Every class in `collections.abc` is built with `ABCMeta`, whose `__instancecheck__` replaces the normal "is this type in the MRO" test with a three-stage answer, cached per class pair: 1. **The structural hook.** If the ABC defines `__subclasshook__`, it runs first and may return `True`, `False`, or `NotImplemented` (meaning "no opinion, keep going"). 2. **The registry.** Classes passed to the ABC's `register()` classmethod — every ABC inherits `ABCMeta.register` from its metaclass, `abc.ABCMeta` — count as virtual subclasses even though they appear nowhere in the MRO. 3. **Real inheritance.** The ordinary subclass relationship. Only a subset of the ABCs supply a hook: `Hashable`, `Iterable`, `Iterator`, `Reversible`, `Sized`, `Container`, `Collection`, `Callable`, `Awaitable` and their kin. Each is defined by one or two dunder names, and the presence of the name is genuinely the whole contract — an object with `__len__` really can be asked for its length. `Sequence` and `Mapping` have no hook at all, so for them stage 1 is skipped and only registration or inheritance can make the check pass. ## Why the line is drawn there It is not an oversight. Two reasons, both worth stating in an interview. First, **the methods are ambiguous**. A sequence and a mapping are both "things with `__getitem__` and `__len__`". Nothing in the method names distinguishes integer positions from arbitrary keys, so a structural hook for `Sequence` would happily accept a key-addressed container and vice versa. Python has an informal duck test for exactly this ambiguity — the `dict()` constructor and `**` unpacking treat an object as a mapping if it has a `keys()` method — but that is a convention, not something an ABC will assert for you. Second, **the contract is behavioural, not structural**. `Sequence` promises an ordering, promises that `obj[0:2]` means something, and promises `index`, `count`, `__contains__`, `__iter__` and `__reversed__`. No inspection of method names can verify any of that. Rather than let the check tell a comfortable lie, the ABC requires you to opt in. ## Your three options, and their costs **Inherit.** Subclassing the ABC makes the check pass and gives you the mixin methods derived from the abstract ones you implement. The cost is a real base class in your MRO and a metaclass, plus the derived mixins are generic implementations that may be far slower than what your data structure could do. **Register.** Calling the ABC's inherited `register()` classmethod — usable as a decorator — makes `isinstance` and `issubclass` return `True` immediately. It adds no methods, checks nothing, and is not undoable. That is its use (labelling a type you do not own, or one implemented in C) and its danger: after registering, every caller who trusted the check now believes your object has `index`, `count` and `__reversed__`. The check has become a promise nobody verified. **Do not check.** Call the object and let it fail. This is the default in most Python code, for good reason: the ABC check is neither necessary nor sufficient for the thing you actually care about, which is whether the operation you are about to perform works. ## Where the check earns its place Use it at a boundary where you must branch on shape rather than on a single call — for example, a configuration or feature-flag loader that accepts either a mapping of flag name to value or a plain sequence of enabled names, and must decide which it was handed before doing anything. There, `isinstance(value, collections.abc.Mapping)` is clearer and safer than `hasattr(value, "keys")`, and much safer than `isinstance(value, dict)`, which rejects every mapping that is not literally a `dict`. Prefer the ABC to a concrete type whenever you do check. And remember the sharpest trap in the other direction: `str` **is** a registered `Sequence`, and so are `bytes`. A function that accepts "a sequence of flag names" and is handed the single string `"dark_mode"` passes every check and then quietly iterates eleven characters. The ABC check will not save you; an explicit `isinstance(value, str)` rejection at the top of the function will. That asymmetry — the check being too permissive here and too strict there — is the practical reason experienced Python code leans on duck typing and reserves ABC checks for genuine forks in the road.

  • What does registering a class as a virtual subclass of `collections.abc.Sequence` actually give you?
    Only the answer to `isinstance` and `issubclass`. Registration records the class as a virtual subclass; it copies no methods, runs no verification, does not appear in the MRO, and cannot be undone. It is the right tool for labelling a type you do not own, and the wrong one for a class you wrote, because callers who trust the check now assume `index`, `count` and `__reversed__` exist when they may not.
  • How would you tell a mapping from a sequence at runtime when both define `__getitem__`?
    The idiomatic duck test is a `keys()` method — that is exactly what the `dict()` constructor and `**` unpacking use to decide an object is mapping-shaped. The explicit version is `isinstance(value, collections.abc.Mapping)`, which is honest about being a check for a declared contract rather than an inspection. What you should not do is branch on `isinstance(value, dict)`, which rejects every mapping that is not literally a `dict`.
  • Why is accepting "any Sequence" a bug magnet when a caller might pass a string?
    `str` and `bytes` are registered as sequences, so they pass the check and then iterate as characters or ints. A function meant to take a collection of names will silently process eleven one-character names when handed one eleven-character string, with no error anywhere. The standard defence is an explicit early rejection of `str` and `bytes` before the general path, since no type check downstream will catch it.

saying these in an interview costs you the question

  • Thinking isinstance always inspects the object's methods
  • Claiming register() copies mixin methods onto the class
  • Assuming anything with __getitem__ is a Sequence
  • Using isinstance(x, dict) to accept any mapping
  • Forgetting that str passes the Sequence check
  • Believing an ABC check proves the behaviour, not just the label

context