When inheriting directly from `object`, should I call super().__init__()?

  • A+
Category:Languages

Since this question is about inheritance and super, let's begin by writing a class. Here's a simple everyday class that represents a person:

class Person:     def __init__(self, name):         super().__init__()          self.name = name 

Like every good class should, it calls its parent constructor before initializing itself. And this class does its job perfectly well; it can be used with no problems:

>>> Person('Tom') <__main__.Person object at 0x7f34eb54bf60> 

But when I try to make a class that inherits from both Person and another class, things suddenly go wrong:

class Horse:     def __init__(self, fur_color):         super().__init__()          self.fur_color = fur_color  class Centaur(Person, Horse):     def __init__(self, name, fur_color):         # ??? now what?         super().__init__(name)  # throws TypeError: __init__() missing 1 required positional argument: 'fur_color'         Person.__init__(self, name)  # throws the same error 

Because of diamond inheritance (with the object class at the top), it's not possible to initialize Centaur instances correctly. The super().__init__() in Person ends up calling Horse.__init__, which throws an exception because the fur_color argument is missing.

But this problem wouldn't exist if Person and Horse didn't call super().__init__().

This raises the question: Should classes that inherit directly from object call super().__init__()? If yes, how would you correctly initialize Centaur?


Disclaimer: I know what super does, how the MRO works, and how super interacts with multiple inheritance. I understand what's causing this error. I just don't know what the correct way to avoid the error is.


Why am I asking specifically about object even though diamond inheritance can occur with other classes as well? That's because object has a special place in python's type hierarchy - it sits at the top of your MRO whether you like it or not. Usually diamond inheritance happens only when you deliberately inherit from a certain base class for the goal of achieving a certain goal related to that class. In that case, diamond inheritance is to be expected. But if the class at the top of the diamond is object, chances are that your two parent classes are completely unrelated and have two completely different interfaces, so there's a higher chance of things going wrong.

 


If Person and Horse were never designed to be used as base classes of the same class, then Centaur probably shouldn't exist. Correctly designing for multiple inheritance is very hard, much more than just calling super. Even single inheritance is pretty tricky.

If Person and Horse are supposed to support creation of classes like Centaur, then Person and Horse (and likely the classes around them) need some redesigning. Here's a start:

class Person:     def __init__(self, *, name, **kwargs):         super().__init__(**kwargs)         self.name = name  class Horse:     def __init__(self, *, fur_color, **kwargs):         super().__init__(**kwargs)         self.fur_color = fur_color  class Centaur(Person, Horse):     pass  stevehorse = Centaur(name="Steve", fur_color="brown") 

You'll notice some changes. Let's go down the list.

First, the __init__ signatures now have a * in the middle. The * marks the beginning of keyword-only arguments: name and fur_color are now keyword-only. It's nearly impossible to get positional arguments to work safely when different classes in a multiple inheritance graph take different arguments, so for safety, we require arguments by keyword. (Things would be different if multiple classes needed to use the same constructor arguments.)

Second, the __init__ signatures all take **kwargs now. This lets Person.__init__ accept keyword arguments it doesn't understand, like fur_color, and pass them on to down the line until they reach whatever class does understand them. By the time the parameters reach object.__init__, object.__init__ should receive empty kwargs.

Third, Centaur doesn't have its own __init__ any more. With Person and Horse redesigned, it doesn't need an __init__. The inherited __init__ from Person will do the right thing with Centaur's MRO, passing fur_color to Horse.__init__.

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: