pythontypeerrormultiple-inheritance

Python Multiple Inheritance generates "TypeError: got multiple values for keyword argument"


I'm running into a problem using multiple inheritance in Python. In my code, Child class A inherits from two Parent classes, B and C, both abstract base classes. Each of the Parent classes, B and C, inherit from a common Parent (or Grandparent) class D. Class D is also an abstract base class.

Instantiating my Child class generates the following error:

TypeError: __main__.D.__init__() got multiple values for keyword argument 'd'

This looks to me like it might be related to the classic diamond problem?

    class D
    /     \
class B  class C
    \     /
    class A

But as I understand it, Python's Method Resolution Order is supposed to address this problem.

Displaying the MRO for each class:

print(f"MRO: {[x.__name__ for x in A.__mro__]}")
print(f"MRO: {[x.__name__ for x in B.__mro__]}")
print(f"MRO: {[x.__name__ for x in C.__mro__]}")
print(f"MRO: {[x.__name__ for x in D.__mro__]}")

returns exactly what I would expect to see:

MRO: ['A', 'B', 'C', 'D', 'ABC', 'object']
MRO: ['B', 'D', 'ABC', 'object']
MRO: ['C', 'D', 'ABC', 'object']
MRO: ['D', 'ABC', 'object']

This minimal-ish code block reproduces the issue:

from abc import ABC, abstractmethod

class D(ABC,):
    def __init__(
        self,
        _D__arg=None,
        *args,
        **kwargs,
    ):
        self.arg = _D__arg
        super(D, self).__init__(*args, **kwargs,)

    @abstractmethod
    def d_abstract_base_class(self):
        pass


class C(D, ABC,):
    def __init__(
        self,
        _C__arg=None,
        *args,
        **kwargs,
    ):
        self.arg = _C__arg
        super(C, self).__init__(
            _D__arg=self.arg,
            *args,
            **kwargs,
        )

    def d_abstract_base_class(self):
        return True
    @abstractmethod
    def c_abstract_base_class(self):
        pass


class B(D, ABC,):
    def __init__(
        self,
        _B__arg=None,
        *args,
        **kwargs,
    ):
        self.arg = _B__arg
        super(B, self).__init__(
            _D__arg=self.arg,
            *args,
            **kwargs,
        )

    def d_abstract_base_class(self):
        return True
    @abstractmethod
    def b_abstract_base_class(self):
        pass


class A(B,C,):  
    def __init__(
        self,
        _A__arg=None,
        *args,
        **kwargs,
    ):
        self.arg = _A__arg
        super(A, self).__init__(
            _B__arg=self.arg,
            _C__arg=self.arg,
            *args,
            **kwargs,
        )

    def b_abstract_base_class(self):
        return True
    def c_abstract_base_class(self):
        return True


a = A(a=1,)

Similar questions referencing this error in SO do not appear to be addressing multiple inheritance as a root cause.

The code block above, doesn't appear to have any of the common issues addressed in these related questions including: missing "self" argument in method definitions, positional / keyword argument collisions, out of order positional args, etc.

edit: InSync suggested this earlier question: What does 'super' do in Python? which has a lot of great info, some excellent explanations, and was definitely worth the read! However, it doesn't appear to address this specific question since my code is already using super. I did try switching to the Python3 approach to calling super: super().__init__() which eliminated the error, but didn't pass args to the parent classes--which is kind of the reason I'm trying to do this.

Am I misunderstanding multiple inheritance in Python? Or some more general Pythonic concept?

If it helps:


Solution

  • The wrong part in this code is just what the error message reads: You can pass an argument either as positional, or as named - but not as both.

    And you are making your calls inserting explicit nam arguments to the c, b and d parameters placing any position-defined arguments.

    Actually, since you have non-matching initial parameters to your subclasses, the ideal thing for you would be not to use positional arguments at all, and just require any arguments, to any subclass, to be named: that would avoid a lot of headache. Still, there can be space for anonymous positional parameters there, as long as you pass them in the correct order. (pass the first argument, be it c, d or whatever as positional only, then *args).

    Also, note that classes B and C can't "know" they are calling "D" in super() (one of them won't be when instantiating a) and you are passing explicit conflicting values to the d argument.

    That said, let me rewrite something similiar to your example (sams the superfluous abstract methods, since they add nothing to the question), in a way that would work.

    from abc import ABC, abstractmethod
    
    class D(ABC,):
        def __init__(self, d=None, *args, **kwargs):
            # By declaring the first argument as either named or positional with 
            # a default, any call here _EITHER_ has `d` as a named argument
            # _OR_ has any positional parameters. One can't have both. 
            # we are better not allowing any positional only arguments (doing
            # that for the following classes):
            
            self.d = d
            super().__init__(*args, **kwargs,)
            
        def __repr__(self):
            return f"Instance of {self.__class__.__name__} with attrs: {self.__dict__}"
    
    
    class C(D):
        def __init__(self, c=None, *, **kwargs):
            self.c = c
            # Note that the immediate super() class is determined at
            # runtime. If there is another "d" value incomming from a super() call
            # from "B.__init__" you will get an error message.
            
            # so you can't do:
            #super().__init__(d=self.c, **kwargs,)
            # But this is possible and will override any other value for "d"
            kwargs["d"] = self.c
            super().__init__(**kwargs)
    
    class B(D):
        def __init__(self, b=None, *, **kwargs):
            self.b = b 
            kwargs["d"] = self.b  # (may be overriden if C is in the MRO)
            super().__init__(**kwargs,)
    
    class A(B,C):
        def __init__(self, a=2, *, **kwargs):
            self.a = a 
            kwargs["c"] = kwargs["b"] = self.a
            # alternatively, just add c and b values if they are not given:
            # if "c" not in kwargs: kwargs["c"] = self.a 
            # ...
            super(A, self).__init__(**kwargs,)
    
    
    a = A(a=1,)