Python Constructor Inheritance: How to Call Parent __init__
python constructor inheritance: Learn how constructor inheritance works in Python, when to call super().__init__(), and how to handle multiple inheritance and MRO.
Python constructor inheritance often confuses developers new to object-oriented programming because the parent's __init__ is not automatically called when you define a subclass. If you override __init__ in the child class, you must explicitly invoke the parent's constructor, typically with super().__init__(). This article explains how constructor inheritance works, when to call the parent constructor, and how to handle multiple inheritance without breaking the chain.
How Python Handles __init__ in Inheritance
In Python, __init__ is an ordinary method, not a special construct like in Java or C++. When you create a subclass without defining its own __init__, the parent's __init__ is inherited and used automatically. This works fine when the child class adds no new attributes or behavior that requires initialization.
class Animal: def __init__(self, name): self.name = name class Dog(Animal): pass d = Dog("Rex") print(d.name) # Rex
However, the moment you define an __init__ in the subclass, you override the parent's constructor entirely. Python does not implicitly call the parent's version. If you need the parent's initialization logic, you must call it yourself.
Using super().__init__() to Call the Parent Constructor
The standard way to call the parent constructor is through super(). This function returns a proxy object that delegates method calls to the next class in the method resolution order (MRO). In a single inheritance scenario, that is the parent class.
class Animal: def __init__(self, name): self.name = name class Dog(Animal): def __init__(self, name, breed): super().__init__(name) self.breed = breed d = Dog("Rex", "Labrador") print(d.name, d.breed) # Rex Labrador
Using super() is preferable to directly naming the parent class (Animal.__init__(self, name)) because it keeps the code DRY and works correctly with multiple inheritance. It also respects the MRO, which becomes critical when a class hierarchy involves more than one parent.
What Happens When You Don't Call the Parent Constructor
If you override __init__ but forget to call super().__init__(), the parent's initialization logic never runs. This often leads to AttributeError when you try to access attributes that the parent constructor would have set.
class Animal: def __init__(self, name): self.name = name class Dog(Animal): def __init__(self, name, breed): self.breed = breed # No super().__init__() d = Dog("Rex", "Labrador") print(d.name) # AttributeError: 'Dog' object has no attribute 'name'
Even if the parent constructor has no side effects, skipping it can break invariants that the parent class expects. For example, if the parent class registers instances in a global list or initializes a logging handler, that behavior is silently lost.
Multiple Inheritance and the MRO
When a class inherits from multiple parents, the behavior of super() changes. Instead of calling a single parent, super() follows the C3 linearization algorithm to determine the next class in the MRO. This allows cooperative multiple inheritance, where each class in the chain can cooperate to ensure all constructors run.
class A: def __init__(self): print("A init") super().__init__() class B: def __init__(self): print("B init") super().__init__() class C(A, B): def __init__(self): print("C init") super().__init__() c = C()
The output is:
C init
A init
B init
Notice that B's constructor is called even though B is not a parent of A. This works because super() in A delegates to the next class in the MRO, which is B. For this chain to work, every class in the hierarchy must call super().__init__() — even if they are not directly derived from each other. If one link omits the call, the chain breaks and remaining constructors do not execute.
Common Mistakes and Pitfalls
One frequent mistake is using the parent class name instead of super(). While it works in single inheritance, it breaks cooperative multiple inheritance because it bypasses the MRO and can cause a parent's constructor to execute twice.
class A: def __init__(self): print("A init") super().__init__() class B: def __init__(self): print("B init") super().__init__() class C(A, B): def __init__(self): print("C init") A.__init__(self) # Wrong: calls A directly, then A calls super() -> B c = C()
Here A.__init__ is called explicitly, and inside A, super() resolves to B, so B runs as well. But if you call both A.__init__ and B.__init__ manually, you may end up with duplicate initialization. Always use super() to let the MRO manage the order.
Another pitfall is mismatched arguments. The parent constructor may expect parameters that the child does not pass. In such cases, use *args and **kwargs in the child's __init__ to forward unknown arguments, but be careful: this can hide signature errors. A better approach is to define explicit parameters that match the parent's signature when you know the hierarchy.
Constructor Inheritance with Different Signatures
When the child constructor needs a different set of parameters, you can accept the parent's parameters and add new ones. This keeps the interface clear and avoids relying on *args unless necessary.
class Vehicle: def __init__(self, make, model): self.make = make self.model = model class Car(Vehicle): def __init__(self, make, model, doors): super().__init__(make, model) self.doors = doors
If you need to support arbitrary parent signatures, especially in a mixin or a framework where you don't control the parent, *args and **kwargs are a pragmatic fallback:
class Base: def __init__(self, **kwargs): self.config = kwargs class Child(Base): def __init__(self, **kwargs): super().__init__(**kwargs) self.extra = kwargs.get("extra")
This pattern is common in frameworks like Django or SQLAlchemy, but it sacrifices signature clarity. Use it only when the parent's constructor is not stable or when you are building a generic mixin.
When to Avoid Calling the Parent Constructor
There are cases where you deliberately do not call super().__init__(). For example, if the parent class is an abstract base class that only defines an interface and has no meaningful initialization, you might skip it. Another case is when the parent's constructor has expensive side effects that the child wants to avoid, but this is a design smell. If you find yourself skipping the parent constructor often, reconsider the class hierarchy.
A safer pattern is to make the parent's __init__ a no-op or to use abstract base classes with abc.ABCMeta to prevent instantiation. In such cases, you can omit the call without breaking invariants, but you should document why.
from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): pass class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return 3.14159 * self.radius ** 2
Here Shape has no __init__, so calling super().__init__() is unnecessary. The abstract method ensures that Shape cannot be instantiated directly.
Maintainability and Design Considerations
Constructor inheritance has a direct impact on maintainability. When every class in a hierarchy calls super().__init__(), you can rely on the MRO to initialize all parts of an object. This is especially valuable in frameworks where mixins are combined. However, it requires discipline: every class must cooperate, and the order of initialization becomes part of the contract.
To keep constructors maintainable, follow these guidelines:
- Always call
super().__init__()in every class that overrides__init__, unless you have a concrete reason not to. - Keep constructor signatures consistent across the hierarchy. If you need to add parameters, add them with defaults or use keyword arguments.
- Avoid deep inheritance chains; prefer composition over inheritance when possible.
- Document the expected MRO order when using multiple inheritance, so future maintainers understand which constructor runs first.
A practical technique is to use a base class that defines a common __init__ signature and then have all subclasses call it with super(). This reduces duplication and makes the initialization flow predictable.
class Base: def __init__(self, **kwargs): self.created_at = kwargs.get("created_at") self.updated_at = kwargs.get("updated_at") class User(Base): def __init__(self, **kwargs): super().__init__(**kwargs) self.username = kwargs.get("username") class Admin(User): def __init__(self, **kwargs): super().__init__(**kwargs) self.role = "admin"
This pattern allows each class to add its own attributes while ensuring that the base initialization always runs. It also makes it easy to add new fields without touching every subclass.
Runtime Cost of Constructor Inheritance
Calling super().__init__() has negligible runtime overhead—it is just a method call. The real cost comes from the complexity of the MRO lookup, which is cached after the class is created. For typical application code, this is not a performance bottleneck. However, in hot loops where you instantiate many objects, the overhead of attribute assignment inside constructors can matter more than the inheritance mechanism. If you are creating millions of objects per second, consider using __slots__ to reduce memory usage and attribute lookup time, but that is a separate optimization.
In practice, constructor inheritance is more about correctness and maintainability than performance. The main risk is not speed but rather subtle bugs caused by forgetting to call super().__init__() or by breaking the MRO chain in multiple inheritance.
Handling Edge Cases with __init__ and super()
One edge case is when the parent class does not define __init__. In that case, calling super().__init__() is harmless; it resolves to object.__init__, which takes no arguments. However, if you pass arguments to it, you will get a TypeError. So it is safe to call super().__init__() without arguments when the parent has no explicit constructor, but passing arguments requires the parent to accept them.
Another edge case is when a class inherits from a built-in type like dict or list. These types have their own __init__ that expects specific arguments. To extend them, you must call the parent constructor with the correct arguments.
class MyDict(dict): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.default = kwargs.get("default", None)
Here super().__init__(*args, **kwargs) forwards everything to dict.__init__, which handles the mapping initialization. This pattern is necessary to preserve the behavior of the built-in type.
Finally, be aware that super() works not only in __init__ but in any method. The same MRO principles apply. Understanding super() in constructors gives you a solid foundation for using it elsewhere, such as in __new__ or custom methods that need to cooperate across a hierarchy.