Plugin registry: abstract intermediates and a broken __init_subclass__ chain
Your team maintains validkit, a validation library whose abstract base Validator(ABC) declares validate(self, value) and uses __init_subclass__ to add every subclass to a module-level REGISTRY keyed by a lowercased slug. There are 40 concrete validators. To share code, the team inserts intermediate classes such as NumericValidator(Validator) and then RangeValidator(NumericValidator). Two problems appear: REGISTRY now contains the six intermediate classes, and calling NumericValidator() raises TypeError: Can't instantiate abstract class NumericValidator with abstract method validate. Separately, class LoggedRange(LoggingMixin, RangeValidator) never reaches the registry, although LoggedRange().validate(3) works. LoggingMixin defines __init_subclass__ to attach a logger, and its __init__ calls super().__init__(). A code review also notes that RangeValidator.__init__ calls Validator.__init__(self) directly rather than using super(). You must fix registration without forcing every plugin author to remember a decorator, and explain the inheritance behaviour to the team.
1. Explain why the intermediate classes are registered and why `LoggedRange` is not. Propose a registration mechanism that registers only concrete validators, and state one trap with `inspect.isabstract()` inside `__init_subclass__` when `ABCMeta` is involved.
Read the worked answer
__init_subclass__ is an implicit classmethod looked up on the nearest base in the new class's MRO. It runs for every subclass created by type, including intermediate classes that still have abstract methods. That is why the six intermediates land in REGISTRY: NumericValidator is a subclass of Validator, even though it cannot be instantiated. LoggedRange is missed for the opposite reason: LoggingMixin is first in the MRO, so its __init_subclass__ shadows Validator.__init_subclass__; if it does not call super().__init_subclass__(**kwargs), the chain stops before the registry code runs. Two rules follow: every __init_subclass__ in a mixin must be cooperative, and registration should not depend on implicit inheritance.
A robust fix is to register lazily on first successful instantiation in Validator.__init__ (type(self) is concrete by then), or to use an explicit @register decorator. Do not simply use inspect.isabstract(cls) inside __init_subclass__: with ABCMeta, __abstractmethods__ is populated by ABCMeta.__new__ after type.__new__ has already called __init_subclass__, so isabstract() can return False for an abstract intermediate. If you need class-creation-time registration, override ABCMeta.__new__, call super().__new__, then check cls.__abstractmethods__. Finally, normalize the slug and raise on duplicates at import time rather than silently overwriting.
2. The MRO of `LoggedRange` is `LoggedRange, LoggingMixin, RangeValidator, NumericValidator, Validator, ABC, object`. `RangeValidator.__init__` calls `Validator.__init__(self)` and `NumericValidator.__init__` calls `super().__init__()`. What runs when `LoggedRange(...)` is constructed, which initializer is skipped, and how do you repair it? Include the rule you would enforce in review.
Read the worked answer
Instantiation calls the first __init__ found on the instance: LoggedRange does not define one, so LoggingMixin.__init__ runs. Its super().__init__() resolves dynamically from the instance's MRO: next after LoggingMixin is RangeValidator, so that runs. But RangeValidator.__init__ calls Validator.__init__(self) directly. That jumps over NumericValidator.__init__, so any numeric bounds setup there never happens. Validator.__init__ then continues with its own super(), which goes to ABC/object; the chain is not broken after that point, but it has already skipped a class. The symptom is usually missing attributes at validation time, not an immediate exception.
Repair: every __init__ in the hierarchy must be cooperative - accept **kwargs, do its own work, and call super().__init__(**kwargs). In RangeValidator, replace Validator.__init__(self) with super().__init__(**kwargs). In NumericValidator, keep its super().__init__(**kwargs) call. Mixins should not assume they are last, and concrete classes should not call grandparent methods by name. Review rule: no direct Base.__init__(self, ...) in a class that participates in multiple inheritance; add a test with a sentinel mixin that records whether its initializer ran.
3. A new experimental plugin family should be excluded from the default registry while the existing 40 validators keep registering automatically. Extend the `__init_subclass__` protocol to support this. What keyword-handling detail is easy to get wrong?
Read the worked answer
Use a class keyword consumed by __init_subclass__:
class Validator(ABC):
__register__ = True
def __init_subclass__(cls, *, register=True, **kwargs):
super().__init_subclass__(**kwargs)
cls.__register__ = register
A plugin then writes class Experimental(Validator, register=False): .... The important detail is **kwargs: __init_subclass__ must receive and forward unknown keywords, or keywords intended for another base raise TypeError. Setting __register__ on the base provides a default for classes that never use the keyword, so registration code can read cls.__register__ directly instead of using a fallback that might collide with a domain attribute. Keeping the default True preserves the existing 40 registrations. Document that a class opting out must also ensure its concrete subclasses opt back in, otherwise a concrete subclass of Experimental silently disappears from the registry. This is preferable to a decorator if the requirement is no plugin-author action, but an explicit decorator is easier to reason about; if you need both, let the decorator set the same class attribute before registration is resolved.