skip to content

Why is isinstance(obj, collections.abc.Iterable) True for a class that never subclassed or registered with it?

level: middleimportance: nice to knowfreq 20%

answer

  1. isinstance on an ABC is a method call
  2. The ABC gets to answer for itself
  3. Only the single-method ABCs do this
  4. A classmethod consulted by ABCMeta
  5. __subclasshook__ looks for the dunder in the MRO

basics

~10 s

collections.abc.Iterable defines subclasshook, a classmethod that ABCMeta consults during every issubclass check. It looks for iter anywhere in the candidate class's MRO and answers True, so no inheritance and no registration are needed.

solid answer

~40 s

An `isinstance` check against an ABC is a method call on `ABCMeta`, not a walk of the MRO. Before considering real subclassing or registered virtual subclasses, it calls the ABC's `__subclasshook__`, which may return `True`, `False`, or `NotImplemented` to fall through to the normal rules. `collections.abc.Iterable` implements the hook to look for `__iter__` in the `__dict__` of any class in the candidate's MRO, so a plain class defining `__iter__` passes. The stdlib hooks guard themselves with an identity test on the ABC, so subclasses do not inherit the duck-typing. Only the single-method ABCs do this — `Iterable`, `Iterator`, `Sized`, `Container`, `Hashable`, `Callable`, `Collection` and relatives. `Sequence`, `Mapping` and `Set` deliberately do not, because `__getitem__` plus `__len__` does not imply sequence semantics.

code

python · 14 lines
python
import collections.abc

class Catalogue:
    def __iter__(self):
        yield "amphora"

print(isinstance(Catalogue(), collections.abc.Iterable))

class Shelf:
    def __getitem__(self, index): return index
    def __len__(self): return 17

print(isinstance(Shelf(), collections.abc.Sized))
print(isinstance(Shelf(), collections.abc.Sequence))

go deeper

for a junior

Know that some abstract base classes accept any class that defines the right special method, so a class with iter passes an Iterable check without inheriting anything. Recognising that isinstance is not always about the MRO is enough here.

for a middle

Name the mechanism and its shape: subclasshook is a classmethod on the ABC that ABCMeta calls during issubclass, returning True, False or NotImplemented, and Iterable uses it to look for iter in the candidate's class dictionaries.

for a senior

Show where it stops helping. The hook is presence-only, reads classes rather than instances, and the multi-method ABCs opt out on purpose — so an isinstance check against Iterable proves far less about an object than most code assumes it does.

for a principal

Decide when your own abstract bases should duck-type at all. A hook is a public promise that a bare method name is sufficient evidence, it runs on every uncached check, and it is much harder to withdraw later than an explicit registration policy.

### The hook `ABCMeta` implements `__subclasscheck__`, so every `issubclass(candidate, SomeABC)` — and every `isinstance`, which goes through the same path — is a method call on the ABC's metaclass rather than a walk of `candidate.__mro__`. Before falling back to real subclassing and to explicitly registered virtual subclasses, that check calls `SomeABC.__subclasshook__(candidate)`. `__subclasshook__` is a `classmethod` an ABC may define. It returns: * `True` — treat the candidate as a subclass, * `False` — definitely not, stop asking, * `NotImplemented` — no opinion, fall through to the ordinary rules (real inheritance, then registration). `object.__subclasshook__` returns `NotImplemented`, so an ABC that defines nothing behaves nominally. ### What `collections.abc.Iterable` does with it `Iterable`'s hook checks whether `__iter__` appears in the `__dict__` of any class in the candidate's MRO. If it does, the hook returns `True`. That is why this is `True` with no inheritance and no registration: ```python class Catalogue: def __iter__(self): yield "amphora" isinstance(Catalogue(), collections.abc.Iterable) # True ``` The stdlib hooks guard themselves with an identity test — roughly `if cls is Iterable: ...` else `return NotImplemented` — so the duck-typing applies only to the ABC that defined it. A user-written subclass of `Iterable` does not accidentally inherit the "anything with `__iter__` counts" rule. ### Which ABCs do this, and which deliberately do not The one-method ABCs opt in: `Iterable`, `Iterator`, `Sized`, `Container`, `Hashable`, `Callable`, `Reversible`, `Collection`, `Awaitable`, `Coroutine`, `AsyncIterable`, `AsyncIterator`, `Buffer` and a few relatives. The multi-method, semantics-carrying ABCs do not: `Sequence`, `Mapping`, `Set`, `MutableMapping` and friends have no hook. The reason is that a method name can only be evidence for a *single-verb* protocol. Having `__iter__` really does mean "you can iterate me". Having `__getitem__` and `__len__` does not mean you are a sequence — a mapping has both, and nothing in the names promises integer indexing, slicing, or `index`/`count` semantics. So this is the outcome: ```python class Shelf: def __getitem__(self, index): return index def __len__(self): return 17 isinstance(Shelf(), collections.abc.Sized) # True - hook isinstance(Shelf(), collections.abc.Sequence) # False - no hook ``` To be a `Sequence` you must subclass it or register — and subclassing is usually what you want, because inheriting from `Sequence` gives you `__contains__`, `__iter__`, `__reversed__`, `index` and `count` for free, built on the `__getitem__` and `__len__` you supply. That trade is the actual design: the ABCs that hand you free mixin methods make you ask for them, and the ABCs that hand you nothing are willing to guess. ### The limits of the hook * **It reads classes, not instances.** An `__iter__` assigned onto an instance in `__init__` does not make it `Iterable` — the hook scans the MRO's class dictionaries, and special-method lookup skips the instance anyway. * **It is presence-only.** No signature, no arity, no return-type check. A class whose `__iter__` takes three required arguments and returns `None` still passes. * **It says nothing about `__next__`.** `Iterable` and `Iterator` are separate ABCs; a class with only `__iter__` is iterable but is not an iterator, and the distinction is the one most often muddled in answers. * **It is a runtime check.** A static structural check performed by a type checker is a different mechanism with different rules; do not conflate the two. ### Writing your own Defining `__subclasshook__` on your own ABC is legitimate when the protocol really is one method and you want third-party classes to satisfy it without touching them. Keep it cheap — it runs on every uncached `isinstance` — guard it with the `cls is MyABC` identity test, and return `NotImplemented` rather than `False` when you have no opinion, so that explicit registration and real inheritance still work. ### The short answer `isinstance` on an ABC is a metaclass method, not an MRO walk. `collections.abc.Iterable` implements `__subclasshook__` to answer "does this class define `__iter__` anywhere in its MRO?", so any class with the method qualifies without inheriting or registering. Structural ABCs like `Sequence` refuse to do that on purpose, because their names do not imply their semantics.

  • Why does collections.abc.Sequence not duck-type on __getitem__ and __len__?
    Because those names carry no semantics. A mapping has both, and nothing in the names promises integer indexing, slicing, or the ordering that `index` and `count` assume. `Sequence` therefore requires real inheritance or registration — and inheriting is usually what you want, since it hands you `__contains__`, `__iter__`, `__reversed__`, `index` and `count` built on the two methods you supply.
  • What do you get by subclassing collections.abc.Mapping instead of just defining the dunders?
    You implement `__getitem__`, `__iter__` and `__len__` — its three abstract methods — and inherit `keys`, `items`, `values`, `get`, `__contains__`, `__eq__` and `__ne__` as mixins, plus the abstract-method check that stops you shipping a half-built class. Duck-typing gives you none of that; it only makes some `isinstance` checks pass.
  • Does assigning __iter__ onto an instance make that object Iterable?
    No. The hook inspects the class dictionaries along the MRO, so an attribute set in `__init__` is invisible to it. Special-method lookup for implicit invocation skips the instance anyway, so such an object would not iterate in a `for` loop either — the two rules agree, which is why dunders belong on the class.

saying these in an interview costs you the question

  • Says isinstance against an ABC always walks the MRO
  • Thinks inheritance or registration is required for every ABC
  • Believes the hook checks the method's signature
  • Assumes every collections.abc ABC duck-types the same way
  • Confuses this runtime check with a static structural check
  • Thinks an instance attribute named __iter__ counts

context