pythonoopmixinssignature

How to preserve an informative `__init__` signature when using parameterized Mixins in Python?


In my Python project, I heavily use Mixins as a design pattern, and I’d like to continue doing so. However, I am facing an issue with the __init__ method signatures in the final class. Since I am passing arguments through **kwargs, the resulting signature is not helpful for introspection or documentation or type checking. Here’s an example to illustrate the issue:

class Base:
    def __init__(self, arg1):
        self.arg1 = arg1

class ParamMixin1:
    def __init__(self, arg2, **kwargs):
        super().__init__(**kwargs)
        self.arg2 = arg2

class ParamMixin2:
    def __init__(self, arg3, **kwargs):
        super().__init__(**kwargs)
        self.arg3 = arg3

class NonParamMixin:
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

class Derived(NonParamMixin, ParamMixin1, ParamMixin2, Base):
    pass

d = Derived(arg1=1, arg2=2, arg3=3)

from inspect import signature
print(signature(Derived.__init__))

This prints:

(self, **kwargs)

The signature is not very helpful since all arguments are hidden under **kwargs. I could technically rewrite the __init__ methods like this to expose the full signature:

class Base:
    def __init__(self, arg1):
        self.arg1 = arg1

class ParamMixin1:
    def __init__(self, arg2, arg1):
        super().__init__(arg1)
        self.arg2 = arg2

class ParamMixin2:
    def __init__(self, arg3, arg2, arg1):
        super().__init__(arg2, arg1)
        self.arg3 = arg3

class NonParamMixin:
    def __init__(self, arg3, arg2, arg1):
        super().__init__(arg3, arg2, arg1)

class Derived(NonParamMixin, ParamMixin1, ParamMixin2, Base):
    pass

from inspect import signature
print(signature(Derived.__init__))

This works, and the signature is more informative, but it introduces a lot of boilerplate and requires the correct ordering of arguments, which sometimes doesn't matter depending on the mixins used. Furthermore sometimes only a subset of the mixins is used.

I've tried creating a metaclass to override the __init__ signature dynamically, but it became very messy, and I couldn’t get it to work reliably.

However, it feels counterintuitive not to have access to the proper __init__ signature. Without it, I’d need to manually track all the mixins and their required parameters, which seems impractical. Surely, there’s a better way to manage this?

Any suggestions or alternative approaches to achieve a cleaner, more maintainable solution would be greatly appreciated as well!


Solution

  • from dataclasses import dataclass
    from inspect import signature
    
    
    @dataclass(kw_only=True)
    class Base:
        arg1: int
    
    
    @dataclass(kw_only=True)
    class ParamMixin1:
        arg2: int
    
    
    @dataclass(kw_only=True)
    class ParamMixin2:
        arg3: int
    
    
    @dataclass
    class NonParamMixin:
        pass
    
    
    @dataclass
    class Derived(NonParamMixin, ParamMixin1, ParamMixin2, Base):
        pass
    
    
    d = Derived(arg1=1, arg2=2, arg3=3)
    # d = Derived(1, 2, 3)  # raises an error because of kw_only=True
    
    print(signature(Derived.__init__))
    # (self, *, arg1: int, arg3: int, arg2: int) -> None
    # * means that all the arguments after * are keyword-only arguments
    # This way you don't have to worry about order