Allow object.__init_subclass__ to take keyword arguments

When defining a custom __init_subclass__ for a class, it’s almost always a good practice to call super().__init_subclass__ to make the class cooperative with other classes which may also have __init_subclass__ defined. And that cooperation includes passing on any given keyword arguments.

PEP-487 suggests as much, with the first demonstrated use case of __init_subclass__ being one with a call to super().__init_subclass__(**kwargs).

And yet, the actual implementation of object.__init_subclass__ does not take keyword arguments, making it unnecessarily difficult to call super().__init_subclass__(**kwargs) without fear of producing a TypeError.

Consider the following use case where both Foo and Bar are designed to look for a debug keyword argument when subclassed:

class Foo:
    def __init_subclass__(cls, **kwargs):
        if kwargs.get('debug'):
            print(f'initialized from {Foo.__init_subclass__}')
        super().__init_subclass__(**kwargs)

class Bar:
    def __init_subclass__(cls, **kwargs):
        if kwargs.get('debug'):
            print(f'initialized from {Bar.__init_subclass__}')
        super().__init_subclass__(**kwargs)

class Baz(Foo, Bar, debug=True):
    pass

This outputs:

initialized from <bound method Foo.__init_subclass__ of <class '__main__.Foo'>>
initialized from <bound method Bar.__init_subclass__ of <class '__main__.Bar'>>
Traceback (most recent call last):
  File "./prog.py", line 13, in <module>
  File "./prog.py", line 5, in __init_subclass__
  File "./prog.py", line 11, in __init_subclass__
TypeError: Baz.__init_subclass__() takes no keyword arguments

To fix this, object.__init_subclass__ should be made to accept keyword arguments and ignore them.

Doesn’t the same issue apply equally to __init__, BTW?

Pretty sure the standard workaround is to make sure your class hierarchy starts at your own base, instead of object. That’s probably easier than checking for “is object next in MRO?” or handling the exception.

1 Like

It does and it has always bothered me too.

It just so happens that this time I have a real use case involving __init_subclass__ that can’t be elegantly solved without asking for a fix because I’m writing a library for others to use where users are supposed to be able to inherit from my classes in any order.

Not sure what you mean here. If you look at my example the class hierachy does start at my own base. But object is always the real hidden base, hence the problem.

In contrast to __init__, __init_subclass__ generally requires less coordination between the various base classes. You can have one base class using __init_subclass__ to slightly modify the class (for example, act as a dataclass_transform), whereas the other collects subclasses into some kind registry. In contrast, the fact that __init__ sets instance attributes and takes positional arguments means that you need more knowledge about what the sibling classes might want to do.

OTOH, if object.__init_subclass__ starts taking keyword arguments, typos are no longer caught, which IMO represents a worse error.


While typing this, I realized the actual solution: The class should consume the arguments it itself takes and pass only the rest on to super().__init_subclass__. Multiple classes trying to act on the same keyword argument should only be done if those classes actually know each other. So I am against this proposal.

3 Likes

I mean to insert a dummy base that doesn’t call super, or calls it explicitly without arguments, because it knows it will be next to object in MRO.

I see. That feels rather ugly having to force users of my classes to remember inserting a dummy class like that after inheriting from any combination of my classes in any order.

I just came up with what I now think is a reasonably elegant workaround, wtih a base class for all my exported classes that checks whether the current class is the last of my classes in the MRO and consumes my known keyword arguments if it’s indeed the last:

class Base:
    @classmethod
    def safe_super_init_subclass(cls, from_cls, **kwargs):
        mro = cls.mro()
        if not any(issubclass(base, Base)
                for base in mro[mro.index(from_cls) + 1:] if base is not Base):
            kwargs.pop('debug', None)
        super(from_cls, cls).__init_subclass__(**kwargs)

class Foo(Base):
    def __init_subclass__(cls, **kwargs):
        if kwargs.get('debug'):
            print(f'initialized from {Foo.__init_subclass__}')
        cls.safe_super_init_subclass(Foo, **kwargs)

class Bar(Base):
    def __init_subclass__(cls, **kwargs):
        if kwargs.get('debug'):
            print(f'initialized from {Bar.__init_subclass__}')
        cls.safe_super_init_subclass(Bar, **kwargs)

class Baz(Foo, Bar, debug=True):
    pass

I’m pretty sure that the extra logic is not necessary, by virtue of the fact that Foo and Bar inherit Base, this will force Base to come right before object in the MRO without the user code (Baz) needing to worry about it. Certainly that holds for your example:

>>> Baz.__mro__
(<class '__main__.Baz'>, <class '__main__.Foo'>, <class '__main__.Bar'>, <class '__main__.Base'>, <class 'object'>)

So Base should be able to assume unconditionally that its super is object, and the others should be able to assume unconditionally that their super is not object.

1 Like

Thanks. Should’ve thought of that instead of redoing what MRO already does. It does solve the problem elegantly.

class Base:
    def __init_subclass__(cls, **kwargs):
        kwargs.pop('debug', None)
        super().__init_subclass__(**kwargs)

class Foo(Base):
    def __init_subclass__(cls, **kwargs):
        if kwargs.get('debug'):
            print(f'initialized from {Foo.__init_subclass__}')
        super().__init_subclass__(**kwargs)

class Bar(Base):
    def __init_subclass__(cls, **kwargs):
        if kwargs.get('debug'):
            print(f'initialized from {Bar.__init_subclass__}')
        super().__init_subclass__(**kwargs)

class Baz(Foo, Bar, debug=True):
    pass

Maybe I’m missing something in what you’re asking.

If your init-subclass takes a “debug” kwarg, then it only takes that one, and others are none of its business. Passing it a “gubed” kwarg is an error if and when no other parent in the mro takes a “gubed” kwarg.
Having your init-subclass discard unknown kwargs without passing them to the super would prevent that hypothetical second parent to get the “gubed” kwarg.
Having classes which are not children of that hypothetical other parent, but only of your class, take a “gubed” kwarg and having the object.__init_subclass__ gobble it up with no error, that would be a bug too (because “errors should not pass silently”).

Also, I would tweak your code to the following

class Base:
    def __init_subclass__(cls, debug=None, **kwargs):
        super().__init_subclass__(**kwargs)

class Foo(Base):
    def __init_subclass__(cls, debug=None, **kwargs):
        if debug:
            print(f'initialized from {Foo.__init_subclass__}')
        super().__init_subclass__(debug=debug, **kwargs)

class Bar(Base):
    def __init_subclass__(cls, debug=None, **kwargs):
        if debug:
            print(f'initialized from {Bar.__init_subclass__}')
        super().__init_subclass__(debug=debug, **kwargs)

class Baz(Foo, Bar, debug=True):
    pass
2 Likes

Is the problem described by OP the cause of my error in this post of mine?

Specifically, the fact that object.__init_subclass__ does not take arbitrary kwargs, causes issues when passing class parametrizations to __prepare__, which invariably get funneled directly to __init_subclass__ for some reason?

PEP 3115 introduces the ability to parametrize classes, and then PEP487 shows extended use cases by being able to pass keywords to __init_subclass__ through that mechanism.