skip to content

What does collections.abc.MutableMapping.register(cls) give a class, and what does it not?

level: seniorimportance: nice to knowfreq 18%

answer

  1. A promise nobody checks
  2. The type test changes, the class does not
  3. No methods arrive, no methods are required
  4. It is applied from outside the class
  5. For classes you cannot edit

basics

~20 s

It makes the class a virtual subclass, so isinstance and issubclass answer True. That is all: no mixin methods are added, no abstract method is enforced, and the base class never enters the class's method resolution order.

solid answer

~40 s

`register` records an unenforced promise. After `MutableMapping.register(VendorConfig)`, `isinstance(cfg, MutableMapping)` is `True` and so is `issubclass`, but the class gains nothing else — no `get`, no `keys`, no `update`, and no check that `__getitem__` or `__delitem__` even exist. The base class is not in the class's method resolution order, so there is nothing to inherit from. It returns the class, so it also works as a decorator. Use it for a class you cannot edit — a third-party mapping-like object you would not get changed until the next release train — or where adding a base would collide with the class's own layout. The cost is that the type check now lies: a caller who trusts it and calls `.get()` gets an `AttributeError` at runtime instead of a `TypeError` at class definition.

code

python · 22 lines
python
from collections.abc import MutableMapping


@MutableMapping.register          # returns the class, so it works as a decorator
class VendorConfig:
    def __init__(self):
        self._store = {}

    def __getitem__(self, key):
        return self._store[key]

    def __setitem__(self, key, value):
        self._store[key] = value


cfg = VendorConfig()
print(isinstance(cfg, MutableMapping))            # True: a virtual subclass
print(issubclass(VendorConfig, MutableMapping))   # True
try:
    cfg.get("region")                             # no mixins were inherited
except AttributeError as exc:
    print("AttributeError:", exc)

go deeper

for a junior

Know that there are two ways a class can be reported as a mapping: it inherited the interface, or someone declared from the outside that it satisfies it. Only the first one actually gives the class any methods.

for a middle

Be able to state the asymmetry crisply: inheriting enforces five methods and hands back a dozen; registering enforces nothing and hands back nothing. Explain the AttributeError a caller gets when it trusts the type check on a registered class.

for a senior

Show when you would reach for it — a class from a dependency, or one whose layout cannot take another base — and how you pay the debt: a conformance test that exercises the full interface, because you removed the language's own check.

for a principal

Own the policy. An unenforced type claim spreads through a codebase and quietly outlives the conditions that justified it, so decide where registration is allowed at all, and what a team must ship alongside one.

## Two ways to claim you are a mapping Inheriting from `collections.abc.MutableMapping` is a *checked* claim: the five abstract methods must exist or the class cannot be instantiated, and in return the mixins arrive. Registering is an *unchecked* one: `MutableMapping.register(SomeClass)` makes `SomeClass` a **virtual subclass**, and the only thing that changes is the answer to a type test. ```python from collections.abc import MutableMapping @MutableMapping.register # returns the class, so it works as a decorator class VendorConfig: ... ``` ## What registration actually does * `isinstance(obj, MutableMapping)` and `issubclass(VendorConfig, MutableMapping)` become `True`. * `register` returns the class it was handed, which is why the decorator form works. * Nothing else. The base class does **not** appear in the registered class's method resolution order, so no method is inherited: no `get`, no `__contains__`, no `keys`, no `pop`, no `update`, no `setdefault`, no `__eq__`. * No abstract method is enforced, at registration or ever. You can register a class with a single `__getitem__` and no way to delete anything, and Python will never object. That asymmetry is the whole question. Inheritance costs you five methods and pays you a dozen; registration costs you nothing and pays you nothing except the type answer. ## Why the mechanism exists **You do not own the class.** The common case: a mapping-like object comes out of a dependency, your code and other people's code want to type-check it as a mapping, and you cannot add a base class to a class you did not define. Subclassing it changes the type that gets constructed, which the dependency's own factory functions will not do for you. Registration is applied from the outside, at import time, to a class as it already is — and if the fix has to wait for that dependency's next three-week release train, registration is what you have today. **Inheriting is impossible or expensive.** A class implemented in C, or one with an incompatible layout or its own metaclass, may not accept another base at all. Registration sidesteps the inheritance graph entirely. **Retrofitting an interface onto an existing hierarchy.** A family of classes written before anyone thought of the interface can be declared to satisfy it without touching their bases. ## What it costs The type check now carries a promise nobody verifies. Code downstream that does the natural thing — check the type, then use the interface — will call a mixin method that was never inherited and get `AttributeError: 'VendorConfig' object has no attribute 'get'`, at runtime, in whatever code path first reached it. Compare that with inheritance, where a missing `__delitem__` is a `TypeError` the first time anyone constructs the object, with the missing method named in the message. Worse, the promise decays silently. Register a class today because it happens to implement the whole surface; six months later someone renames a method during a refactor, every test that only exercises the paths they touched still passes, and the registration keeps insisting the class is a mapping. Nothing in the language will ever re-check it. ## There is no partial version A frequent hope is that registration might at least bring the mixins along without the enforcement, or that inheritance might be persuaded to skip the abstract-method check. Neither exists. The two mechanisms are the opposite ends of one lever: inheritance hands you a dozen methods and demands five in return, registration demands nothing and hands back nothing but the answer to a type test. If you want the convenience methods on a class you cannot inherit into, you either implement them, or you wrap the object in a thin class of your own that does inherit and delegates the five abstract methods to it — which buys the enforcement back as well. ## How to decide * **You own the class and can inherit** — inherit. You get enforcement and the mixins, and the enforcement is what stops the drift described above. * **You do not own it, or it cannot take another base** — register, and pay the debt deliberately: write a test that drives the full mapping surface against the registered class so the unenforced promise has at least one enforcer of your own making. * **The class only implements part of the interface** — do not register it as `MutableMapping`. Registering a read-only object as mutable is the version of this that turns into a bug report, because callers are entitled to call `__setitem__` on anything the check accepted. If it is read-only, `Mapping` is the honest claim. * **You control the call sites too** — consider not registering at all and simply calling the methods; a promise that nobody queries buys nothing. The short version for an interview: registration moves a claim from the type system into a comment that `isinstance` happens to believe.

  • If a registered class already implements the whole mapping surface, is registration harmless?
    It is correct today and unenforced forever. Nothing re-checks the class when someone renames a method or drops one during a refactor, and no test will fail unless it happens to exercise that path. If you register, own the promise yourself: a test that drives every operation the interface advertises against the registered class is the only thing standing in for the enforcement you gave up.
  • What happens if you register a class that has no __delitem__?
    Nothing, ever, until someone calls it. Registration performs no inspection at registration time and none afterwards, so the class is happily reported as a mutable mapping and the missing method surfaces as an `AttributeError` deep in a caller. That is the argument for registering the read-only interface instead when the object genuinely cannot delete.
  • Why can registration be the only option for a class from a dependency?
    You cannot add a base class to a class you did not define, and subclassing it does not help when the dependency's own code constructs the original type. Registration is applied from the outside to the class object as it stands, so one call at import time makes every instance the dependency creates satisfy the check.

Inheriting is passing the exam; registering is signing a form saying you would have passed. Both get you the badge, only one was checked.

saying these in an interview costs you the question

  • Thinks register copies the mixin methods onto the class
  • Expects register to raise when required methods are missing
  • Believes the base class joins the class's method resolution order
  • Registers a read-only class as a mutable mapping
  • Uses register on a class it owns instead of inheriting
  • Assumes a registered class stays conformant after a refactor

context