Understanding Python super() with __init__() methods
Categories:
Mastering Python's super()
with __init__()
in Inheritance

Explore the super()
function in Python, its role in cooperative multiple inheritance, and how to correctly use it with __init__()
methods to build robust class hierarchies.
In Python, super()
is a powerful built-in function that provides a way to call methods from a parent or sibling class. While often associated with single inheritance, its true strength shines in complex multiple inheritance scenarios, enabling cooperative method resolution. This article delves into the mechanics of super()
, focusing specifically on its application within __init__()
methods, which is crucial for proper object initialization across an inheritance chain.
The Basics of super()
and __init__()
When a class inherits from another, it often needs to extend or modify the parent's initialization logic. The __init__()
method is where an object's initial state is set up. Without super()
, you might explicitly call the parent's __init__()
using ParentClass.__init__(self, ...)
. While this works for single inheritance, it becomes problematic with multiple inheritance because it bypasses Python's Method Resolution Order (MRO) and can lead to methods being called multiple times or not at all.
class Parent:
def __init__(self, name):
self.name = name
print(f"Parent __init__ called for {self.name}")
class Child(Parent):
def __init__(self, name, age):
# Explicitly calling parent's __init__
Parent.__init__(self, name)
self.age = age
print(f"Child __init__ called for {self.name}, age {self.age}")
child_obj = Child("Alice", 30)
# Output:
# Parent __init__ called for Alice
# Child __init__ called for Alice, age 30
Explicitly calling a parent's __init__()
method.
The super()
function, on the other hand, dynamically determines the next method to call in the MRO. This makes your code more maintainable and robust, especially when dealing with complex class hierarchies. It ensures that all __init__()
methods in the inheritance chain are called exactly once and in the correct order.
class Parent:
def __init__(self, name):
self.name = name
print(f"Parent __init__ called for {self.name}")
class Child(Parent):
def __init__(self, name, age):
# Using super() to call the next __init__ in the MRO
super().__init__(name) # No need to pass 'self'
self.age = age
print(f"Child __init__ called for {self.name}, age {self.age}")
child_obj = Child("Bob", 25)
# Output:
# Parent __init__ called for Bob
# Child __init__ called for Bob, age 25
Using super().__init__()
for cooperative initialization.
super()
, you do not need to pass self
as the first argument. Python automatically handles this. super().__init__(args)
is equivalent to super(Child, self).__init__(args)
.Understanding Method Resolution Order (MRO)
The MRO is the sequence in which Python looks for a method in a class hierarchy. When super()
is called, it consults the MRO to find the next class in the chain that implements the method being called. You can inspect a class's MRO using ClassName.__mro__
or help(ClassName)
.
classDiagram class Grandparent { +__init__(value) } class Parent1 { +__init__(value) } class Parent2 { +__init__(value) } class Child { +__init__(value) } Grandparent <|-- Parent1 Grandparent <|-- Parent2 Parent1 <|-- Child Parent2 <|-- Child
A simple class hierarchy demonstrating multiple inheritance.
Consider a diamond inheritance pattern, a common scenario in multiple inheritance. Without super()
, calling parent __init__
methods explicitly could lead to the Grandparent.__init__
being called twice, or not at all if the order is incorrect. super()
elegantly solves this by ensuring each __init__
is called once, following the MRO.
class Grandparent:
def __init__(self, value):
self.value = value
print(f"Grandparent __init__ called with {value}")
class Parent1(Grandparent):
def __init__(self, value, p1_data):
super().__init__(value)
self.p1_data = p1_data
print(f"Parent1 __init__ called with {value}, {p1_data}")
class Parent2(Grandparent):
def __init__(self, value, p2_data):
super().__init__(value)
self.p2_data = p2_data
print(f"Parent2 __init__ called with {value}, {p2_data}")
class Child(Parent1, Parent2):
def __init__(self, value, p1_data, p2_data, child_data):
super().__init__(value, p1_data=p1_data, p2_data=p2_data) # Pass all expected args
self.child_data = child_data
print(f"Child __init__ called with {value}, {p1_data}, {p2_data}, {child_data}")
print("Child MRO:", Child.__mro__)
# Expected MRO: (<class '__main__.Child'>, <class '__main__.Parent1'>, <class '__main__.Parent2'>, <class '__main__.Grandparent'>, <class 'object'>)
child_obj = Child(10, "P1", "P2", "C")
# Output:
# Grandparent __init__ called with 10
# Parent2 __init__ called with 10, P2
# Parent1 __init__ called with 10, P1
# Child __init__ called with 10, P1, P2, C
Cooperative initialization in a diamond inheritance pattern using super()
.
__init__
calls in the diamond example. Parent2.__init__
is called before Parent1.__init__
because Parent2
appears earlier in Child
's MRO after Parent1
. This highlights why super()
is crucial for correct cooperative initialization.Best Practices for super()
with __init__()
To effectively use super()
in your __init__()
methods, follow these guidelines:
1. Always use super()
Make it a habit to use super().__init__(...)
instead of direct parent calls like ParentClass.__init__(self, ...)
. This ensures your code is robust against changes in the inheritance hierarchy.
2. Pass all arguments up the chain
Each __init__()
method in the hierarchy should accept all arguments that its super()
call might need, even if it doesn't directly use them. This often means using *args
and **kwargs
to pass along arguments that are not explicitly handled by the current class but are expected by a class further up the MRO.
3. Handle arguments explicitly or pass them on
In each __init__()
method, explicitly extract and handle the arguments relevant to that class, then pass the remaining arguments (or all arguments if none are consumed) to super().__init__()
. This ensures that every class in the MRO receives the arguments it expects.
class Base:
def __init__(self, *args, **kwargs):
print(f"Base __init__ called with args={args}, kwargs={kwargs}")
# No super() call here, as Base is the top of this specific chain
class Mixin1:
def __init__(self, m1_arg, *args, **kwargs):
self.m1_arg = m1_arg
print(f"Mixin1 __init__ called with m1_arg={m1_arg}, args={args}, kwargs={kwargs}")
super().__init__(*args, **kwargs)
class Mixin2:
def __init__(self, m2_arg, *args, **kwargs):
self.m2_arg = m2_arg
print(f"Mixin2 __init__ called with m2_arg={m2_arg}, args={args}, kwargs={kwargs}")
super().__init__(*args, **kwargs)
class FinalClass(Mixin1, Mixin2, Base):
def __init__(self, final_arg, m1_arg, m2_arg, base_arg):
self.final_arg = final_arg
print(f"FinalClass __init__ called with final_arg={final_arg}")
# Pass all arguments that might be needed by parents
super().__init__(m1_arg=m1_arg, m2_arg=m2_arg, base_arg=base_arg)
# Inspect MRO
print("FinalClass MRO:", FinalClass.__mro__)
# Create an instance
obj = FinalClass(final_arg="F", m1_arg="M1", m2_arg="M2", base_arg="B")
# Output will show the __init__ calls in MRO order, passing arguments correctly.
Advanced super()
usage with *args
and **kwargs
for flexible initialization.