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.
- 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
Python inheritance is a mechanism where a class (child) derives behavior and state from another class (parent), enabling code reuse and establishing an 'is-a' relationship. It exists to model hierarchical taxonomies—like Manager inheriting from Employee—without duplicating logic.
In Python, inheritance is deceptively simple: you pass the parent class in parentheses (class Child(Parent):), but the real complexity lies in method resolution order (MRO), cooperative multiple inheritance via C3 linearization, and the function, which delegates to the next class in the MRO chain, not just the direct parent. Missing super() calls in overridden methods silently breaks this chain, causing state corruption or skipped initialization—a common production bug that can cascade through frameworks like Django or SQLAlchemy, where base classes rely on super() for setup.super()
In the Python ecosystem, inheritance is a core tool but not always the right one. Single inheritance is straightforward: override methods, call to ensure parent initialization runs. Multilevel inheritance (A -> B -> C) and multiple inheritance (C inherits from A and B) introduce real trade-offs: diamond problems, fragile base classes, and MRO confusion.super().__init__()
Frameworks like Django’s class-based views or Flask’s class-based views lean heavily on inheritance, but misuse—like forgetting in a mixin—can break entire request pipelines. Alternatives like composition (using super()has-a relationships via dependency injection or delegation) often yield more testable, flexible code.
The rule of thumb: prefer composition over inheritance unless you genuinely model an is-a hierarchy and need polymorphic behavior; inheritance adds coupling that can make migrations and refactoring painful.
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 super() is a common source of bugs in ORM models, context managers, and framework base classes. Always call super().__init__() in __init__, and super().__exit__() in __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()
# Real-world example: a generic BankAccount and a specialised SavingsAccount class BankAccount: """The parent class — holds logic every bank account needs.""" def __init__(self, owner: str, balance: float = 0.0): self.owner = owner self.balance = balance # shared attribute every account type needs def deposit(self, amount: float) -> None: """Add funds. Validation lives here once, not in every subclass.""" if amount <= 0: raise ValueError("Deposit amount must be positive.") self.balance += amount print(f"Deposited £{amount:.2f}. New balance: £{self.balance:.2f}") def withdraw(self, amount: float) -> None: """Base withdrawal — no special rules yet.""" if amount > self.balance: raise ValueError("Insufficient funds.") self.balance -= amount print(f"Withdrew £{amount:.2f}. Remaining: £{self.balance:.2f}") def __repr__(self) -> str: return f"BankAccount(owner='{self.owner}', balance=£{self.balance:.2f})" class SavingsAccount(BankAccount): # <-- inherits from BankAccount """Child class — adds an interest rate and enforces a minimum balance.""" MINIMUM_BALANCE = 100.0 # class-level rule specific to savings accounts def __init__(self, owner: str, balance: float = 0.0, interest_rate: float = 0.03): # super().__init__ calls BankAccount.__init__ so we don't duplicate that logic super().__init__(owner, balance) self.interest_rate = interest_rate # SavingsAccount-specific attribute def withdraw(self, amount: float) -> None: """Override parent's withdraw to enforce minimum balance rule.""" if (self.balance - amount) < self.MINIMUM_BALANCE: raise ValueError( f"Cannot go below minimum balance of £{self.MINIMUM_BALANCE:.2f}." ) # Delegate the actual withdrawal logic back to the parent — DRY! super().withdraw(amount) def apply_interest(self) -> None: """New method that only SavingsAccount has — not on the parent.""" interest_earned = self.balance * self.interest_rate self.balance += interest_earned print(f"Interest applied: £{interest_earned:.2f}. Balance: £{self.balance:.2f}") def __repr__(self) -> str: return ( f"SavingsAccount(owner='{self.owner}', " f"balance=£{self.balance:.2f}, rate={self.interest_rate:.1%})" ) # --- Usage --- account = SavingsAccount(owner="Alice", balance=500.0, interest_rate=0.05) print(account) account.deposit(200.0) # inherited from BankAccount — no code duplication account.apply_interest() # only available on SavingsAccount account.withdraw(100.0) # overridden version — checks minimum balance try: account.withdraw(560.0) # this should fail — would breach minimum balance except ValueError as error: print(f"Blocked: {error}") print(account)
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.
# Multilevel inheritance: Vehicle -> Car -> ElectricCar # Multiple inheritance via a Mixin: adding GPS capability cleanly class Vehicle: """Top of the chain — every vehicle has these basics.""" def __init__(self, make: str, model: str, year: int): self.make = make self.model = model self.year = year def start_engine(self) -> str: return f"{self.make} {self.model} engine started." def __repr__(self) -> str: return f"{self.year} {self.make} {self.model}" class Car(Vehicle): """Multilevel level 2 — a Car IS-A Vehicle with door-count logic.""" def __init__(self, make: str, model: str, year: int, num_doors: int = 4): super().__init__(make, model, year) # pass shared args up the chain self.num_doors = num_doors def honk(self) -> str: return "Beep beep!" class GpsNavigationMixin: """ A mixin — NOT a standalone class, no __init__ of its own. It adds GPS behaviour to any class that includes it. """ def get_current_location(self) -> str: # In a real app this would call a GPS API return "51.5074° N, 0.1278° W (London, UK)" def navigate_to(self, destination: str) -> str: return f"Navigating to '{destination}'... Turn right in 200m." class ElectricCar(GpsNavigationMixin, Car): """ Multilevel level 3 + Multiple inheritance. ElectricCar IS-A Car, and also HAS GPS navigation (via mixin). MRO: ElectricCar -> GpsNavigationMixin -> Car -> Vehicle -> object """ def __init__(self, make: str, model: str, year: int, battery_kwh: float): # super().__init__ here follows the MRO — it reaches Car correctly super().__init__(make, model, year) self.battery_kwh = battery_kwh self.charge_level = 100.0 # percentage def start_engine(self) -> str: # Override parent's method — electric cars don't have a combustion engine return f"{self.make} {self.model} motor silently activated. ⚡" def charge_status(self) -> str: return f"Battery: {self.charge_level:.0f}% ({self.battery_kwh} kWh capacity)" # --- Inspect the MRO before using it --- print("MRO:", [cls.__name__ for cls in ElectricCar.__mro__]) print() tesla = ElectricCar(make="Tesla", model="Model 3", year=2024, battery_kwh=82.0) print(tesla) # from Vehicle.__repr__ print(tesla.start_engine()) # overridden in ElectricCar print(tesla.honk()) # inherited from Car print(tesla.charge_status()) # ElectricCar's own method print(tesla.get_current_location()) # from GpsNavigationMixin print(tesla.navigate_to("Heathrow")) # from GpsNavigationMixin
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.
from abc import ABC, abstractmethod from datetime import datetime class PaymentProcessor(ABC): """ Abstract base class — defines the CONTRACT for all payment processors. You cannot instantiate this class directly. Any subclass MUST implement the abstract methods or Python raises TypeError at class creation time. """ def __init__(self, merchant_id: str): self.merchant_id = merchant_id self._transaction_log: list = [] @abstractmethod def process_payment(self, amount: float, currency: str) -> dict: """ Every payment processor must define HOW it handles a payment. The what (a payment happens) is the contract. The how is up to each subclass. """ pass # abstract — no body needed @abstractmethod def refund(self, transaction_id: str) -> bool: """Every processor must support refunds.""" pass # Concrete method — shared by ALL subclasses, no override needed def log_transaction(self, transaction_id: str, amount: float, status: str) -> None: """Non-abstract: logging works the same for every processor.""" entry = { "id": transaction_id, "amount": amount, "status": status, "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), } self._transaction_log.append(entry) print(f"[LOG] Transaction {transaction_id}: {status} — £{amount:.2f}") def get_transaction_history(self) -> list: return self._transaction_log class StripeProcessor(PaymentProcessor): """Concrete implementation — fulfils the PaymentProcessor contract via Stripe.""" def process_payment(self, amount: float, currency: str) -> dict: # In reality: call stripe.PaymentIntent.create(...) transaction_id = f"stripe_txn_{int(datetime.now().timestamp())}" print(f"Stripe: charging £{amount:.2f} {currency} via card on file...") self.log_transaction(transaction_id, amount, "SUCCESS") return {"processor": "Stripe", "transaction_id": transaction_id, "status": "SUCCESS"} def refund(self, transaction_id: str) -> bool: # In reality: call stripe.Refund.create(payment_intent=transaction_id) print(f"Stripe: processing refund for {transaction_id}...") return True class PayPalProcessor(PaymentProcessor): """Different concrete implementation — same contract, completely different internals.""" def __init__(self, merchant_id: str, client_id: str): super().__init__(merchant_id) # call parent __init__ first self.client_id = client_id # PayPal-specific credential def process_payment(self, amount: float, currency: str) -> dict: transaction_id = f"pp_txn_{int(datetime.now().timestamp())}" print(f"PayPal: initiating £{amount:.2f} {currency} order via REST API...") self.log_transaction(transaction_id, amount, "PENDING") return {"processor": "PayPal", "transaction_id": transaction_id, "status": "PENDING"} def refund(self, transaction_id: str) -> bool: print(f"PayPal: submitting refund request for {transaction_id}...") return True # --- Prove the contract is enforced --- try: bad_processor = PaymentProcessor(merchant_id="test") # should fail except TypeError as error: print(f"Caught expected error: {error}\n") # --- Use the concrete classes polymorphically --- processors: list[PaymentProcessor] = [ StripeProcessor(merchant_id="merch_stripe_001"), PayPalProcessor(merchant_id="merch_pp_001", client_id="pp_client_abc"), ] for processor in processors: result = processor.process_payment(amount=49.99, currency="GBP") print(f"Result: {result}\n")
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()
# Cooperative multiple inheritance: each class passes kwargs along class LoggingMixin: def __init__(self, **kwargs): print(f"LoggingMixin.__init__ called, kwargs: {kwargs}") self.logged_actions = [] super().__init__(**kwargs) class TimestampMixin: def __init__(self, **kwargs): print(f"TimestampMixin.__init__ called, kwargs: {kwargs}") self.created_at = None super().__init__(**kwargs) class Base: def __init__(self, **kwargs): print(f"Base.__init__ called, kwargs: {kwargs}") # Base is the last in the chain — no super().__init__ pass class MyClass(LoggingMixin, TimestampMixin, Base): def __init__(self, name: str, **kwargs): print(f"MyClass.__init__ called, name={name}, kwargs: {kwargs}") super().__init__(name=name, **kwargs) obj = MyClass(name="test") print(f"\nInstance attributes: {obj.__dict__}") print(f"MRO: {MyClass.__mro__}")
- 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 = Engine() attribute.
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.
# Composition: Car HAS-A Engine, Engine is not a parent class Engine: def start(self) -> str: return "Engine started." def stop(self) -> str: return "Engine stopped." class ElectricEngine(Engine): def start(self) -> str: return "Electric motor humming." class Car: def __init__(self, engine: Engine): self.engine = engine # composition def drive(self) -> str: return f"{self.engine.start()} Car moving." # Runtime flexibility: car1 = Car(engine=Engine()) car2 = Car(engine=ElectricEngine()) print(car1.drive()) print(car2.drive()) # Compare with inheritance — brittle and rigid: class CarWithEngine(Car): # wrong: Car IS-NOT an Engine pass # what if we want PetrolEngine tomorrow? New subclass needed.
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.
# io.thecodeforge class BaseOrder: def __init__(self, order_id: int): self.order_id = order_id self.status = "pending" class PriorityOrder(BaseOrder): def __init__(self, order_id: int, priority: str): # BUG: no super().__init__() self.priority = priority order = PriorityOrder(1001, "high") print(order.order_id) # AttributeError: 'PriorityOrder' object has no attribute 'order_id'
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.
# io.thecodeforge from abc import ABC, abstractmethod class PaymentGateway(ABC): @abstractmethod def charge(self, amount: float) -> str: ... class StripeGateway(PaymentGateway): def charge(self, amount: float) -> str: return f"Charged ${amount} via Stripe" class MockGateway(PaymentGateway): pass # Forgot to implement charge() gateway = MockGateway() # TypeError: Can't instantiate abstract class
@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.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__ClassA.__mro__ClassA.mro()ConcreteClass.__abstractmethods__import inspect; [m for m in inspect.getmembers(ConcreteClass) if inspect.isabstract(m[1])]In each class's method: print(f"{type(self).__name__}.{method_name}")Add a dummy parent at the end of MRO that calls super()super().method()| Aspect | Single Inheritance | Multiple Inheritance |
|---|---|---|
| Syntax | class Child(Parent): | class Child(ParentA, ParentB): |
| Complexity | Low — straightforward to follow | Higher — requires MRO awareness |
| Method resolution | Linear — walks one parent chain | C3 Linearisation (MRO) resolves conflicts |
| Diamond Problem risk | None | Exists — Python's MRO handles it, but you must understand it |
| Best use case | Specialisation of a single type (Car → ElectricCar) | Mixing in orthogonal capabilities (GPS, Logging, Serialisation) |
| super() behaviour | Calls the one direct parent | Follows MRO — may call a sibling class, not just the parent |
| Risk of tight coupling | Medium | Higher — changes to either parent can affect the child unpredictably |
| Real-world examples | Django Model subclasses, SQLAlchemy mapped classes | Mixin patterns in Django (LoginRequiredMixin), Pytest plugins |
Key takeaways
super() instead of hardcoding the parent class namesuper() does NOT always call 'the parent'super() across mixins.Common mistakes to avoid
5 patternsForgetting to call super().__init__() in the child class
super().__init__(args, *kwargs) as the first line of your child's __init__. It ensures the parent's setup runs before your child adds its own attributes on top.Using inheritance when composition is the right answer
Assuming super() in multiple inheritance always calls the direct parent
super() is chained across mixin classes. Bugs are reproducible but hard to reason about.super() will call next. In multiple inheritance, super() calls the NEXT class in the MRO, which may be a sibling mixin — not your parent class. Use **kwargs in all cooperating classes.Not using abstract base classes to enforce contracts
Creating mixins with their own __init__ that doesn't call super().__init__()
super().__init__(kwargs). This keeps the cooperative inheritance chain intact.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?
What's the difference between overriding a method and overloading it? Python doesn't support traditional overloading — how would you simulate it in a subclass?
If you have `class C(A, B)` and both A and B define a method called `save()`, and A's `save()` calls `super().save()`, will B's `save()` ever get called? Walk me through why.
save() will be called if the MRO of C is C -> A -> B -> object. When you call c.save() on an instance of C, Python looks up save() in C, doesn't find it, then finds it in A. A's save() calls super().save(). super() looks at the next class in the MRO after A, which is B. So B's save() is called. If B's save() also calls super().save(), then it would reach object.save() (which raises AttributeError unless overridden). This is cooperative multiple inheritance in action. The chain works only if every class in the hierarchy calls super().save().How would you design a class hierarchy for a payment system where you need to support multiple processors (Stripe, PayPal) each with different authentication methods, but share logging and retry logic? Explain your choice between inheritance and composition.
Frequently Asked Questions
Single inheritance means a class inherits from exactly one parent, keeping the hierarchy simple and easy to trace. Multiple inheritance means a class inherits from two or more parents simultaneously. Python handles method conflicts in multiple inheritance using the MRO (Method Resolution Order), calculated via the C3 Linearisation algorithm. Multiple inheritance is most useful for mixin patterns — adding isolated capabilities like logging or serialisation to a class without making it a subtype of those things.
Use inheritance when you can honestly say 'Child IS-A Parent' — a SavingsAccount IS-A BankAccount. Use composition when you'd say 'Child HAS-A thing' — a Car HAS-A Engine, so Engine should be an attribute, not a parent class. Deep inheritance hierarchies become brittle quickly; composition keeps classes loosely coupled and individually testable.
super() is not simply 'call the parent'. It returns a proxy object that delegates method calls to the next class in the MRO, not necessarily the direct parent. In single inheritance this distinction doesn't matter — the next class IS the parent. But in multiple inheritance, super() in a mixin might call a sibling class's method, enabling cooperative multiple inheritance where every class in the chain gets to participate. This is why all classes in a cooperative hierarchy must call super(). even if they don't need it themselves.__init__()
No. Python's abc module prevents instantiation of any class that has any abstract method (decorated with @abstractmethod) that hasn't been overridden in a concrete subclass. Attempting to instantiate an abstract class raises a TypeError: 'Can't instantiate abstract class ... with abstract methods ...' This is enforced at class creation time, not at method call time, making it a powerful tool for catching missing implementations early.
Use ClassName.__mro__ (returns a tuple of class objects) or ClassName.mro() (returns a list). The order is the order in which Python resolves methods. You can also call the inspect.getmro() function. For debugging, print([c.__name__ for c in ClassName.__mro__]) to see a readable list.
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
That's OOP in Python. Mark it forged?
7 min read · try the examples if you haven't