`__init_subclass__` allows positional only

I stumbled upon this last night and felt a little confused as to why this is currently allowed:

class Test:
    def __init_subclass__(cls, test, /) -> None:
        print(test)


class Test2(
    Test,
):
    ...

since it’s impossible to provide that argument when subclassing.

As for regular positional arguments, the exception currently says TypeError: Test.__init_subclass__() missing 1 required positional argument: 'test', although when actually providing it, it is functionally a keyword only argument.

Would it make sense to give more informative errors/tracebacks for these issues?

2 Likes

I think the idiomatic use of __init_subclass__ is to never give it positional-only arguments (apart from the first cls argument). Seems to be a bit of a flaw in the stdlib implementation that the method does allow this kind of definition – perhaps a bit of a disconnect between the syntax and the semantics of that function (positional-only args were introduced in Python 3.8, while __init_subclass__ was added in 3.6, so perhaps core devs never considered this usecase?).

2 Likes

One other thing I noticed is that the PEP states

As an example, the first use case looks as follows:

>>> class QuestBase:
...    # this is implicitly a @classmethod (see below for motivation)
...    def __init_subclass__(cls, swallow, **kwargs):
...        cls.swallow = swallow
...        super().__init_subclass__(**kwargs)

>>> class Quest(QuestBase, swallow="african"):
...    pass

>>> Quest.swallow
'african'

The base class object contains an empty __init_subclass__ method which serves as an endpoint for cooperative multiple inheritance. Note that this method has no keyword arguments, meaning that all methods which are more specialized have to process all keyword arguments.

If I’ve understood correctly, I wonder if this was meant to support calling __init_subclass__ of all bases of a subclass? Because currently iirc it only called the first one when I was testing.

On second thought: I didn’t try calling super() so I could be wrong here :grin:

I think in any real code you definitely need to make sure you call super().__init_subclass__(**kwargs), since you want to make sure that any kwargs are passed on to the base classes. But - on the other hand - this is only really important, imo, if you’re developing a library. If you’re developing an app and know that nothing special is needed in base classes (you know the whole mro chain of all classes), you could decide to leave this out (and doing so might only come back to bite you later, when you forget about this and add a deeper subclass…). When you call super (and all base classes in their __init_subclass__ methods also do so), then all base classes can update their class variables accordingly.

1 Like