Python Inheritance: Missing super() Breaks Migration
During bank migration, a missing super(). left SavingsAccount fields empty.
__init__()
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Single inheritance: one child, one parent — the 90% case you'll use daily
- Multiple inheritance: a class inherits from two+ parents — Python resolves conflicts via MRO (C3 Linearisation)
- super() is MRO-aware — never hardcode the parent name
- Abstract base classes (ABC + @abstractmethod) enforce contracts at definition time
- Biggest mistake: using inheritance when composition fits — 'IS-A' must be true
Think of a smartphone. Every smartphone on the market — iPhone, Samsung, Pixel — shares a common set of features: a screen, a battery, a camera, the ability to make calls. The engineers who designed each phone didn't invent 'screen technology' from scratch for every single model. They started with a blueprint for 'what every phone does', then added their own special sauce on top. That's exactly what inheritance is in Python. You write a base 'blueprint' class once, and every other class that needs those features just borrows them — and then adds its own twist.
Every non-trivial Python application you'll ever work on — from a Django web app to a data pipeline — will involve objects that share behaviour. Maybe you're building a payment system with CreditCardPayment and PayPalPayment classes. Maybe you're modelling a vehicle fleet with Cars, Trucks, and Motorcycles. Without inheritance, you'd copy the same methods into every class, and the moment a requirement changes, you'd hunt down every copy to fix it. That's a maintenance nightmare waiting to happen.
Inheritance solves the 'copy-paste class' problem by letting one class absorb the attributes and methods of another. The parent class (also called a base or superclass) holds the shared logic. Child classes (subclasses) inherit that logic automatically and then extend or override it where they need something different. This keeps your codebase DRY — Don't Repeat Yourself — and makes adding new types trivially easy.
By the end of this article you'll understand not just the syntax of single, multilevel, and multiple inheritance, but — more importantly — you'll know when to reach for each one, what actually does under the hood, how Python resolves method conflicts via the MRO (Method Resolution Order), and the two most common mistakes that trip up even experienced developers. You'll also leave with concrete answers to the interview questions that catch people out.super()
Why Missing super() in Python Inheritance Breaks Production
In Python, inheritance is the mechanism where a child class derives behavior from a parent class. The core mechanic is the MRO (Method Resolution Order), which determines which method runs when you call . When you override a method in a child class and omit self.method(), you break the chain of delegation — the parent's logic never executes. This is not a style choice; it's a contract violation.super().method()
Python's returns a proxy object that follows the MRO, enabling cooperative multiple inheritance. If you skip it, you silently discard the parent's initialization, cleanup, or validation logic. In single inheritance, this often manifests as uninitialized attributes. In multiple inheritance, it can cause entire method chains to be skipped, leading to subtle state corruption that only surfaces under specific call orders.super()
Use inheritance when the child truly is a specialized version of the parent — not just to share code. In real systems, missing is a common source of bugs in ORM models, context managers, and framework base classes. Always call super() in super().__init__()__init__, and in super().__exit__()__exit__, unless you have a documented reason not to.
super() in __init__ of a subclass silently skips parent initialization — no error, just broken state that surfaces later as AttributeError or logic bugs.save() without super().save() silently skips auto_now updates and signal dispatch.super() in overridden methods unless you explicitly want to replace the entire behavior and document why.super() in __init__ is the #1 cause of silent initialization bugs in class hierarchies.super() in one class can break the entire diamond chain — always call it.Single Inheritance — The Foundation You Need to Nail First
Single inheritance is the simplest form: one child class inherits from exactly one parent class. This is the 90% case you'll encounter in real projects.
Here's the key mental model: the child class IS-A version of the parent. A SavingsAccount IS-A BankAccount. A ElectricCar IS-A Car. If you can't truthfully say 'X is a Y', inheritance probably isn't the right tool — you might want composition instead.
When a child class inherits from a parent, it gets every method and attribute the parent defines. It can use them as-is, override them to change their behaviour, or call them via and then extend the result. The super() function is your link back to the parent — it lets the child say 'do everything you normally do, and then I'll add my part on top'. Never hardcode the parent class name inside the child; always use super(). If you rename the parent class later, hardcoding it will silently break your code.super()
BankAccount.__init__(self, ...) inside SavingsAccount works today, but the moment you rename BankAccount or change the class hierarchy, it silently breaks. super() is dynamic — it respects the MRO and always points to the right parent, even in complex multiple-inheritance scenarios.super().__init__() is the #1 production bug in single inheritance.super().__init__() as the first line — every time.super() — never hardcode.Multilevel and Multiple Inheritance — Power Features With Real Trade-offs
Multilevel inheritance is a chain: C inherits from B, which inherits from A. Think of it as a lineage — Grandparent → Parent → Child. This models specialisation naturally. A PremiumSavingsAccount is a kind of SavingsAccount, which is a kind of BankAccount.
Multiple inheritance is Python-specific and more controversial: a single class can inherit from more than one parent at once. This is powerful but requires caution. Python solves the 'Diamond Problem' — where two parent classes share a common grandparent — using the Method Resolution Order (MRO). Python calculates the MRO using the C3 Linearisation algorithm. You can inspect any class's MRO by calling ClassName.__mro__ or ClassName.mro(). When Python looks for a method, it walks this list left to right and uses the first match it finds.
A good real-world use for multiple inheritance is mixins — small, focused classes that add a single capability (like logging or serialisation) without representing a full 'type'. Mixins are designed to be mixed in, not instantiated on their own.
GpsNavigationMixin(), it should work syntactically but makes no semantic sense. Signal this clearly with a docstring and — in larger codebases — by raising NotImplementedError in any method that requires self to be a specific type.super().__init__() with different signatures, the child class breaks.super().__init__.Method Overriding and Abstract Classes — Making Inheritance Safe by Design
Overriding a method means providing a new implementation in the child class that replaces the parent's version. Python picks the child's version first because of the MRO. But here's a subtle problem: what if you want to guarantee that every subclass MUST implement a particular method? Without enforcement, a developer could create a subclass, forget to implement , and the bug only surfaces at runtime — potentially in production.process_payment()
Python's abc module (Abstract Base Classes) fixes this at class-definition time. Mark a method with @abstractmethod and Python will refuse to let you instantiate any subclass that hasn't implemented it. You get a clear TypeError immediately, not a silent failure later.
This pattern is the backbone of frameworks like Django (Model, View), SQLAlchemy (base mappers), and any plugin system. Define the contract in the abstract base class. Every concrete implementation fulfils that contract. This is the 'O' in SOLID — Open/Closed Principle: open for extension, closed for modification.
for processor in processors loop is polymorphism in action. The loop doesn't care whether it's talking to Stripe or PayPal — it just calls .process_payment() and trusts the contract. This is why interviewers love abstract base classes: they demonstrate you understand that inheritance isn't just about reusing code, it's about defining reliable interfaces.The super() Function Deep Dive — What It Actually Does
is not a shortcut for "call the parent". It returns a proxy object that delegates method calls to the next class in the MRO. In single inheritance, that next class is indeed the parent. But in multiple inheritance, super() can call a sibling class — enabling cooperative multiple inheritance.super()
Every class that uses in a method should define its signature to accept super()kwargs when there's any chance it will be part of a diamond hierarchy. This way, each class in the chain can pass along keyword arguments it doesn't need. You'll often see this pattern: def __init__(self, kwargs): . That's cooperative inheritance.super().__init__(**kwargs)
If any class in the chain breaks the chain by NOT calling , the cooperative model fails silently — methods of classes later in the MRO are never called. This is the most common production bug in multiple inheritance.super()
super() as a pointer to the next class in a cooperative line, not a fixed "parent" address.- super() returns a proxy that follows MRO left-to-right.
- In single inheritance, 'next' = parent; in multiple, 'next' = sibling or cousin.
- Always accept **kwargs and pass them through to keep the chain alive.
- If any link in the chain does NOT call
super(), everything after it is dead.
save() but forgets to call super().save() silently disables all parent model logic (signals, auto_now fields).super().method() to preserve the chain.Inheritance vs Composition — When Not to Use Inheritance
Inheritance is overused. The most common mistake is modelling a 'HAS-A' relationship with 'IS-A'. A Car has an Engine — but making Engine a parent of Car makes no semantic sense. A Car isn't an Engine. Use composition: give Car an self.engine = attribute.Engine()
Composition gives you more flexibility at runtime. You can swap the engine type (ElectricEngine vs PetrolEngine) without changing the Car class. With inheritance, you'd need to create a new subclass for each engine type. The 'favor composition over inheritance' principle exists for a reason.
Deep inheritance hierarchies (more than 2–3 levels) become brittle. A change to a grandparent can break unrelated grandchildren. Composition avoids this by keeping classes loosely coupled and individually testable. When in doubt, ask: "Will this hierarchy have more subclasses than methods?" If yes, you've probably overused inheritance.
The Object Super Class — Every Mistake Inherits From Root
Every class you write in Python 3.x silently inherits from object. That's non-negotiable. This hidden root class is why __str__, __repr__, and __eq__ exist on every instance without you typing a single method. But here's where juniors burn production: they override __init__ in a child class and forget to call , breaking the parent's setup. The super().__init__()object class itself does nothing in __init__, so it's harmless for single inheritance. The danger multiplies when you inherit from multiple classes — if any intermediate class expects parameters in __init__ and you skip the chain, attributes silently become None. Your code doesn't crash immediately. It corrupts data three method calls later. Always call in every super().__init__(args, *kwargs)__init__, even when you think it's unnecessary. The only exception is when you explicitly want to break inheritance — and that decision should trigger a code review.
super() chains. Skipping super().__init__() in models silently drops column defaults, triggers ghost errors in migrations, and wastes hours of debugging.object. Every __init__ must call super().__init__(). No exceptions.Abstract Base Classes — Your Contract Against Runtime Chaos
If you're writing a base class and expect child classes to override specific methods, enforce it with abc.ABC and @abstractmethod. Without this, you're praying the next developer reads the docstring. Production doesn't run on prayers. When you decorate a method with @abstractmethod, Python refuses to instantiate any class that hasn't implemented that method. The error happens at instantiation time — not three hours later when an empty method returns None, corrupting a downstream calculation. This is the difference between interface inheritance (what a class promises to do) and implementation inheritance (how it does it). Use abstract base classes to define interfaces. Reserve concrete base classes for shared logic. The abc module also supports @abstractproperty and @abstractstaticmethod for older codebases, but in modern Python, prefer @abstractmethod with regular properties or classmethods. One rule: if your base class's method body contains only pass or raise NotImplementedError, convert it to an abstract base class immediately.
@abstractmethod with __init_subclass__ hooks unless you deeply understand Python's MRO order. We've seen teams break dependency injection frameworks because the ABC metaclass clashed with a custom metaclass. Stick to ABC for interface contracts; leave metaclasses for framework authors.@abstractmethod instead of NotImplementedError.MRO: Method Resolution Order with C3 Linearization
Python's Method Resolution Order (MRO) determines the order in which base classes are searched when executing a method. It uses the C3 linearization algorithm, which ensures a consistent and predictable order, especially in multiple inheritance scenarios. The MRO is crucial for understanding how super() works and why missing super() calls can lead to unexpected behavior.
Consider a classic diamond inheritance problem:
```python class A: def method(self): print("A.method")
class B(A): def method(self): print("B.method") super().method()
class C(A): def method(self): print("C.method") super().method()
class D(B, C): def method(self): print("D.method") super().method() ```
Here, the MRO for class D is computed as D -> B -> C -> A. The C3 algorithm ensures that each class appears after its parents and respects the order of inheritance. If super() is omitted in B or C, the chain breaks, and A.method may never be called. Understanding MRO helps debug such issues.
To view the MRO of any class, use ClassName.__mro__ or ClassName.mro(). For example, D.__mro__ returns (.
In production, always verify MRO when designing complex hierarchies. Tools like can dynamically inspect classes. Remember: missing inspect.getmro()super() calls in any class along the MRO can silently skip important initialization or method logic, leading to subtle bugs.
super() in intermediate classes, which can break the chain silently. Use logging or unit tests to verify that all expected methods are called.super() works correctly.super() with Multiple Inheritance: Cooperative Methods
In multiple inheritance, super() enables cooperative method calls, where each class in the MRO chain can contribute to a method's execution. This pattern is essential for ensuring that all parent classes are properly initialized or have their methods invoked.
Consider a logging mixin and a database class:
```python class LoggingMixin: def __init__(self, args, kwargs): print(f"LoggingMixin.__init__ called") super().__init__(args, **kwargs)
class Database: def __init__(self, args, *kwargs): print(f"Database.__init__ called") # database initialization logic
class MyService(LoggingMixin, Database): def __init__(self, args, kwargs): print(f"MyService.__init__ called") super().__init__(args, **kwargs) ```
When creating MyService(), the MRO is MyService -> LoggingMixin -> Database -> object. Each __init__ calls super(), so all three __init__ methods execute in order. If LoggingMixin omitted super()., Database.__init__ would never run, breaking the service.__init__()
Cooperative methods require that all classes in the hierarchy use super() consistently. This is especially important in frameworks like Django or SQLAlchemy, where mixins and base classes rely on super() for proper initialization.
A common pattern is to accept args, *kwargs in all cooperative methods to pass parameters along the chain. This ensures flexibility and avoids signature mismatches.
In production, enforce super() usage in mixins and base classes via code reviews or linting rules. Missing super() calls in multiple inheritance can cause silent failures that are hard to trace.
super() calls. Consider using dependency injection instead of deep inheritance to reduce complexity.super() to ensure the entire chain executes correctly.Abstract Base Classes vs Protocols: Inheritance vs Duck Typing
Python offers two main ways to define interfaces: Abstract Base Classes (ABCs) and Protocols. ABCs use inheritance to enforce that subclasses implement specific methods, while Protocols rely on structural subtyping (duck typing) where any object with the required methods is considered compatible.
ABCs are defined using the abc module:
```python 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.14 self.radius * 2 ```
Here, Circle must implement , or instantiation will fail. ABCs provide explicit contracts but require inheritance, which can be rigid.area()
Protocols, introduced in PEP 544 (Python 3.8+), allow duck typing with static type checking:
```python from typing import Protocol
class Drawable(Protocol): def draw(self) -> None: ...
class Circle: def draw(self): print("Drawing circle")
class Square: def draw(self): print("Drawing square")
def render(obj: Drawable): obj.draw() ```
Circle and Square don't inherit from Drawable, but they satisfy the protocol. This is more flexible and encourages composition over inheritance.
When to use ABCs vs Protocols? Use ABCs when you need to enforce a contract at runtime (e.g., in frameworks) or when you want to provide default implementations. Use Protocols for static type checking in large codebases where you want to avoid tight coupling.
In production, prefer protocols for interfaces that are used across module boundaries, as they reduce dependencies. ABCs are still valuable for base classes with shared logic.
The Missing super().__init__() Call That Broke Customer Account Migration
super().__init__(). Python never invoked BankAccount.__init__, so all parent attributes (owner, balance) were never set. The child only set interest_rate, leaving balance at default 0.0 and owner as a dangling attribute that later caused an AttributeError in the reporting service.super().__init__(owner, balance) as the first line of SavingsAccount.__init__. Then run a data reconciliation query to re-process the accounts that had zero balance — they actually had funds.- If a child class defines its own __init__, you must explicitly call
super().to initialise parent attributes.__init__() - Always add a sanity check assertion after instantiation in test suites — e.g., assert instance.balance > 0 for SavingsAccount.
- Treat missing parent __init__ as a code review blocker — enforce it with a linter rule (e.g., pylint 'super-init-not-called').
super().__init__(). Print self.__dict__ to see what attributes exist. Compare with parent's __init__ expected attributes.super().method() to maintain cooperative inheritance.instance.__dict__dir(instance)super().__init__() call in child __init__| File | Command / Code | Purpose |
|---|---|---|
| bank_account_inheritance.py | class BankAccount: | Single Inheritance |
| vehicle_hierarchy.py | class Vehicle: | Multilevel and Multiple Inheritance |
| payment_processor_abc.py | from abc import ABC, abstractmethod | Method Overriding and Abstract Classes |
| super_deep_dive.py | class LoggingMixin: | The super() Function Deep Dive |
| composition_vs_inheritance.py | class Engine: | Inheritance vs Composition |
| order_system.py | class BaseOrder: | The Object Super Class |
| payment_gateway.py | from abc import ABC, abstractmethod | Abstract Base Classes |
| mro_example.py | class A: | MRO |
| cooperative_super.py | class LoggingMixin: | super() with Multiple Inheritance |
| abc_vs_protocol.py | from abc import ABC, abstractmethod | Abstract Base Classes vs Protocols |
Key takeaways
super() instead of hardcoding the parent class namesuper() does NOT always call 'the parent'super() across mixins.Interview Questions on This Topic
What is the Method Resolution Order (MRO) in Python, and how does Python's C3 Linearisation algorithm decide which parent's method gets called in a diamond inheritance scenario?
Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
That's OOP in Python. Mark it forged?
8 min read · try the examples if you haven't