Back to Blog
Python

Python super().__init__() Explained

Learn how Python's super().__init__() works, why it matters for inheritance, and how to avoid common mistakes when calling parent constructors.

pythonsuperinitinheritancemro
Diagram showing Python class inheritance with super() in __init__

In Python, super().__init__() is the idiomatic way to initialize inherited attributes from a subclass. It returns a proxy object that delegates method calls to the next class in the method resolution order (MRO). In single inheritance, that next class is the direct parent; in multiple inheritance, it may be another class in a cooperative chain. Understanding that distinction helps you write __init__ methods that work reliably in larger hierarchies.

What super() Actually Does in init

super() returns a proxy object, not the parent class itself. When you call super().__init__(args), Python resolves the call using the MRO of the instance, starting after the class where super() appears. In simple single inheritance, this resolves to the direct parent. In multiple inheritance, it may resolve to a sibling class.

The MRO is computed using the C3 linearization algorithm, which ensures that each class appears before its bases and that the order is consistent with the inheritance graph. You can inspect the MRO with ClassName.__mro__. For a single-inheritance class, the MRO is Child -> Parent -> object; for more complex hierarchies, the order can surprise you if the classes are not designed to cooperate.

Basic super().init() Example

Here is a minimal example:

class Parent: def __init__(self, name): self.name = name class Child(Parent): def __init__(self, name, age): super().__init__(name) self.age = age child = Child("Alice", 30) print(child.name, child.age) # Alice 30

super().__init__(name) calls Parent.__init__, which sets name. Child then sets its own age. Using super() instead of Parent.__init__ keeps the subclass decoupled from the concrete parent name, so the same code keeps working if the inheritance hierarchy changes.

How super() Works in Multiple Inheritance

Consider a diamond-shaped hierarchy:

class A: def __init__(self): print("A.__init__") super().__init__() class B(A): def __init__(self): print("B.__init__") super().__init__() class C(A): def __init__(self): print("C.__init__") super().__init__() class D(B, C): def __init__(self): print("D.__init__") super().__init__() d = D()

The output is:

D.__init__
B.__init__
C.__init__
A.__init__

For D, the MRO is D -> B -> C -> A -> object. Each class delegates to the next class in that MRO, so A.__init__ is called exactly once. This cooperative behavior is the main reason to prefer super() in multiple inheritance. A manual call to a specific parent class bypasses the MRO and can leave other classes in the chain uninitialized.

Common Mistakes with super().init()

One common mistake is calling the parent class directly:

class Child(Parent): def __init__(self, name, age): Parent.__init__(self, name) # Not recommended self.age = age

This may work in single inheritance, but it bypasses the MRO. If Child is later used in a multiple-inheritance hierarchy, other classes in the chain may never be initialized.

Another mistake is forgetting to call super().__init__() at all. If the parent class sets required attributes or performs setup, the child instance may be incomplete and raise AttributeError later.

A third mistake is passing the wrong arguments. In a cooperative hierarchy, each class should accept the arguments it needs and forward the rest. The next section shows the usual pattern.

Passing Arguments Correctly with super().init()

Classes in a cooperative hierarchy often have different parameters. The recommended pattern is to accept every argument the class needs and forward the remaining keyword arguments with **kwargs:

class A: def __init__(self, a, **kwargs): self.a = a super().__init__(**kwargs) class B: def __init__(self, b, **kwargs): self.b = b super().__init__(**kwargs) class C(A, B): def __init__(self, **kwargs): super().__init__(**kwargs) c = C(a=1, b=2) print(c.a, c.b) # 1 2

With C, the MRO is C -> A -> B -> object. A.__init__ consumes a and forwards b; B.__init__ consumes b and calls super().__init__() with no arguments. If any keyword argument is still present when the chain reaches object.__init__, Python raises a TypeError, so make sure each class consumes all arguments it is responsible for.

When Not to Call super().init()

Calling super().__init__() is not always required. If a subclass intentionally replaces the parent's initialization, it can omit the call. This is uncommon and often indicates a design problem, but it is allowed.

The bigger issue is non-cooperative legacy code. If a parent class does not call super().__init__() itself, a subclass that relies on a cooperative chain may not initialize that parent. In such code, you may need to call that parent directly. That direct call is tied to the concrete class and will not respect the full MRO, so use it only when you cannot change the legacy class.

Maintainability and Compatibility

Using super() consistently keeps subclasses decoupled from specific parent names. That makes the code easier to refactor and is especially valuable for framework classes that users may subclass.

In Python 3, the zero-argument form of super() is the standard. Python 2 required super(Child, self).__init__(), which is no longer relevant for new code. The reason to use super() is correct initialization and maintainability, not performance.

In Python, super().__init__() is best understood as cooperative dispatch, not simply a parent constructor shortcut. Following the **kwargs convention and keeping every class in the chain willing to call super() lets you build complex hierarchies without skipping initialization steps.

Python super().__init__() Explained | RYUSLOG DEV