skip to content

How do keyword arguments in `class C(Base, tag='x')` reach `__init_subclass__`?

level: middleimportance: should knowfreq 28%

answer

  1. Configuration written in the class header
  2. Everything but metaclass is forwarded
  3. The hook signature declares what it accepts
  4. Leftovers reach object and raise TypeError
  5. No default means every subclass must supply it

basics

~20 s

Every keyword in a class header except metaclass is forwarded to the parent's init_subclass as a keyword argument, so a hook declared as def init_subclass(cls, /, tag=None, **kwargs) receives tag='x'. Consume what you own before calling super().

solid answer

~40 s

A class statement accepts keyword arguments after the bases, and PEP 487 wired them straight through to the class-creation hook. `metaclass` is special-cased and consumed by the class machinery; everything else is handed to `__init_subclass__` along with the new class, so `class C(Base, tag='x')` calls the hook with `tag='x'`. The hook signature declares the keywords it understands and forwards the rest: `def __init_subclass__(cls, /, tag=None, **kwargs)` then `super().__init_subclass__(**kwargs)`. Anything left over eventually reaches `object.__init_subclass__`, which accepts no keyword arguments and raises `TypeError` at class-definition time — so a misspelled keyword in a class header fails loudly at import instead of being ignored. Declaring a keyword without a default makes it mandatory: subclasses that omit it fail to be created at all.

code

python · 11 lines
python
class Base:
    def __init_subclass__(cls, /, tag=None, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.tag = tag


class Child(Base, tag="tmx"):
    pass


print(Child.tag)

go deeper

for a junior

Know that a class statement can carry keyword arguments after the base list, and that they configure how the class is created rather than how instances are built.

for a middle

Explain the routing precisely: metaclass is consumed by the machinery, everything else lands in the parent's hook, defaults make a keyword optional, and leftovers raise TypeError at import.

for a senior

Show when a header keyword beats a class attribute — creation-time configuration that must not be inherited by accident — and how you keep the cooperative chain intact across several bases that each want their own keywords.

for a principal

Weigh a declarative class-header vocabulary against plain attributes and explicit registration for a library other teams extend: type-checker blindness, discoverability and the cost of import-time failures are the axes to argue on.

## The syntax Since Python 3.0 a class statement has allowed keyword arguments after the base list, and for a long time the only one anybody used was `metaclass=`: ```python class C(Base, tag="tmx"): ... ``` PEP 487 (Python 3.6) gave the rest of them a destination that does not require writing a metaclass. `metaclass` is consumed by the class machinery itself and is *not* forwarded. Every other keyword is passed along to class creation and, from there, to the parent's `__init_subclass__` together with the newly built class. ## Declaring what you accept The hook declares its keywords like any other function and forwards the remainder: ```python class Base: def __init_subclass__(cls, /, tag=None, **kwargs): super().__init_subclass__(**kwargs) cls.tag = tag ``` Three details in that signature earn their place. * **`/`** makes `cls` positional-only. Without it, a subclass writing `class C(Base, cls="x")` would collide with the first parameter and produce a confusing `TypeError`. * **A default** (`tag=None`) makes the keyword optional. Drop the default and the keyword becomes *mandatory*: any subclass that omits it fails at class-definition time with a missing-argument `TypeError`. That is a legitimate, and quite strong, way to force every subclass to declare something. * **`**kwargs` plus the `super()` call** keeps the chain cooperative. If two classes on the MRO both define the hook and both want different keywords, each pops its own and passes the rest up. ## Why unconsumed keywords are an error `object.__init_subclass__` is the end of the chain and it accepts no keyword arguments whatsoever. Forwarding one to it raises a `TypeError` complaining that `__init_subclass__` takes no keyword arguments, raised while the class statement is executing — that is, at import time, with a traceback pointing at the class header. This is a feature rather than an annoyance. Class headers are configuration written in the middle of code, and configuration typos are exactly the failure that silently misbehaves later. Making `class C(Base, tagg="tmx")` blow up at import turns a subtle behavioural bug into a startup error. If you want unknown keywords tolerated, you have to swallow them deliberately by accepting `**kwargs` and never forwarding them — which is worth doing only when you genuinely own an extensible header vocabulary. ## Where else the keywords go The same keywords are also visible to a custom metaclass, which receives them in its own creation hooks. That symmetry is what lets a library migrate from a metaclass to `__init_subclass__` without changing a single class header. The metaclass side of the story belongs to metaclasses proper; what matters here is that the keywords are not exclusive to either mechanism, and that `metaclass=` itself never reaches the hook. ## What the pattern buys you Keyword arguments in the header turn a class statement into a small declarative form: ```python class TmxHandler(Handler, key="tmx", version=2): ... ``` Everything the base needs to know about the subclass is stated in the header rather than in a magic class attribute the base has to go looking for, and the base can validate it immediately. Compared with the alternative of reading class attributes inside the hook (`getattr(cls, "key", None)`), header keywords have three advantages: they cannot be accidentally inherited from a parent, they are visibly *about* class creation rather than about instances, and a missing one is caught by ordinary parameter binding instead of hand-written checks. The counterweight is that header keywords are invisible to type checkers and to readers who do not know the base. A class attribute is self-describing and can be annotated; a header keyword needs the base's documentation. In practice, use keywords for things that configure *creation* — the registry key, an opt-out flag, a validation switch — and class attributes for values the instances themselves need. ## Common mistakes The mistake seen most often is treating the header keywords as constructor arguments: they have nothing to do with `__init__` and are never passed to instances. The second is forgetting `super().__init_subclass__(**kwargs)`, which quietly disables any cooperating hook higher up the MRO. The third is popping a keyword and then forwarding it anyway, which resurrects the `TypeError` you thought you had handled.

  • Which class-header keyword is never forwarded to the hook?
    `metaclass`. The class machinery consumes it to decide which callable builds the class, so it never appears in the keywords handed to `__init_subclass__`. Every other keyword after the base list is forwarded verbatim, which is why a library can accept an arbitrary vocabulary of header options without a metaclass of its own.
  • What happens if you forward a keyword you meant to consume?
    It travels up the MRO until it reaches `object.__init_subclass__`, which accepts no keyword arguments and raises `TypeError` while the class statement is still executing. The fix is to name the keyword in your own signature so ordinary parameter binding removes it, and forward only `**kwargs`.
  • How would you make one header keyword mandatory for every subclass?
    Declare it in the hook signature without a default: `def __init_subclass__(cls, /, key, **kwargs)`. A subclass that omits it fails to be created, with a missing-argument `TypeError` pointing at the class statement. That is stronger than checking a class attribute, because it cannot be satisfied by accidental inheritance from a parent.

saying these in an interview costs you the question

  • Thinks class-header keywords are passed to __init__
  • Believes only metaclass= is allowed in a header
  • Expects an unknown keyword to be ignored silently
  • Forgets to consume a keyword before calling super()
  • Assumes the keywords become class attributes automatically

context