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!
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