Problem using `__init_subclass__` in 'mixins'

I’m trying to use __init_subclass__ in ‘mixins’ but have hit a problem if two __init_subclass__s have the same argument name. EG msg below:

class A:
    def __init_subclass__(cls, msg, **kwargs):
        print(f'A: {msg}')
        super().__init_subclass__(**kwargs)

class B:
    def __init_subclass__(cls, msg, **kwargs):
        print(f'B: {msg}')
        super().__init_subclass__(**kwargs)

class AB(A, B, msg='Hello from AB'): ...

Which gives the error:

A: Hello from AB
Traceback (most recent call last):
  File <snip>, in <module>
    class AB(A, B, msg='Hello from AB'): ...
  File <snip> in __init_subclass__
    super().__init_subclass__(**kwargs)
TypeError: B.__init_subclass__() missing 1 required positional argument: 'msg'

Is there a way round this other than using unique argument names?

If you are doing cooperative inheritance, all classes need to know about each other.

Define a common root for A and B that also has __init_subclass__(self, msg, **kwargs) defined, but ignores the argument. Then both A and B can easily call super().__init_subclass__(msg=msg, **kwargs)

2 Likes

Unfortunately they are unrelated. Just a coincidence that they have the same argument name. But thanks for the suggestion.

The problem is not unique to init_subclass but any method using MRO when unrelated classes are used as mixins.

Yep. Don’t use inheritance then. It’s the wrong tool for combining arbitrary classes.

1 Like

I don’t think there’s necessarily anything wrong with trying to define a class that inherits from both A and B. In the spirit of the adaptor classes suggested under “How to Incorporate a Non-cooperative Class” at Python’s super() considered super! | Deep Thoughts by Raymond Hettinger, you can hack something using some trivial mixin classes that “save” the value of msg before calling A.__init_subclass__, then restores it before calling B.__init_subclass__. For example,

class PreA:
    def __init_subclass__(cls, msg, **kwargs):
        super().__init_subclass__(msg, msg_B=msg, **kwargs)

class PreB:
    def __init_subclass__(cls, msg_B, **kwargs):
        super().__init_subclass__(msg_B, **kwargs)

class AB(PreA, A, PreB, B, msg="Hello from AB"):
    pass

PreA and PreB serve no other purpose than to inject some code in the sequence of __init_subclass__ calls, which produce as output

A: Hello from AB
B: Hello from AB

The definitions of PreA and PreB can be adapted depending on whether you want msg to be passed to only one of A or B, or if you want to pass separate messages to both. In any case, their definitions are carefully designed for this specific definition of AB.

You can patch AB.__bases__ to remove these adaptors, but then subclassing AB will have the same problem as originally trying to subclass A and B simultaneously. For example,

class Cleanup:
    def __init_subclass__(cls, **kwargs):
        cls.__bases__ = tuple(c for c in cls.__bases__ if c not in [PreA, PreB, Cleanup])


class AB(PreA, A, PreB, B, Cleanup, msg="Hello from AB"):
    pass

Thanks @chepner using an adaptor class is a great option.