Skip to content

Inheritance & the MRO Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

Python interview questions on single vs multiple inheritance, the MRO and C3 linearization, super(), the diamond problem, mixins, and abstract base classes.

Read the in-depth guidePython Inheritance and the MRO Explained — super(), the Diamond Problem, and Mixins(opens in new tab)
15 of 15

Single inheritance means a class derives from exactly one parent; multiple inheritance means it lists more than one base class. Python supports both — multiple inheritance is what lets you compose behaviour from several sources at once.

class Animal:
    def eat(self): print("eating")

class Dog(Animal):          # single inheritance
    def bark(self): print("woof")

class Swimmer: ...
class Flyer: ...
class Duck(Animal, Swimmer, Flyer):  # multiple inheritance
    pass

Multiple inheritance is powerful but can create ambiguity about which parent's method wins — that ambiguity is resolved by the MRO. Rule of thumb: prefer single inheritance plus small mixins over deep, wide hierarchies.

The MRO (Method Resolution Order) is the linear, ordered list of classes Python searches when looking up an attribute or method on an instance. CPython builds it with the C3 linearization algorithm, which guarantees a consistent order that respects each class's own order of bases and never places a parent before its child.

class A: ...
class B(A): ...
class C(A): ...
class D(B, C): ...

D.__mro__            # (D, B, C, A, object)
D.mro()             # same, as a list

C3 produces a single deterministic order (or raises TypeError if no consistent order exists). Attribute lookup walks this list left to right and stops at the first match. Rule of thumb: read Cls.__mro__ whenever multiple inheritance surprises you — it tells you exactly who wins.

super() does not simply call "the parent class" — it calls the next class in the instance's MRO, starting after the current class. That cooperative behaviour is what makes multiple inheritance work correctly: each class delegates to whatever comes next, regardless of the static hierarchy.

class A:
    def greet(self): print("A")
class B(A):
    def greet(self): print("B"); super().greet()
class C(A):
    def greet(self): print("C"); super().greet()
class D(B, C):
    def greet(self): print("D"); super().greet()

D().greet()   # D, B, C, A  — follows D.__mro__, not B's parent

Note super().greet() inside B calls C, not A, because the MRO of a D instance puts C after B. Rule of thumb: in a cooperative hierarchy every override should call super() so the whole chain runs exactly once.

The diamond problem arises when two classes (B, C) inherit from a common base (A), and a fourth class (D) inherits from both. The question is: when D calls an inherited method, is A's code run once or twice? Naive languages run it twice; Python's C3 MRO guarantees A appears exactly once, so cooperative super() calls run it a single time.

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

D()   # D, B, C, A  — A's __init__ runs ONCE

The MRO (D, B, C, A, object) linearizes the diamond into a clean chain. Rule of thumb: the diamond is only safe when every class in it uses super() consistently — mixing super() with hard-coded Base.__init__(self) calls breaks the guarantee.

A mixin is a small class that provides a focused slice of behaviour meant to be combined into other classes via multiple inheritance — it isn't useful on its own and usually has no __init__. An abstract base class (ABC), from the abc module, defines an interface with @abstractmethods and cannot be instantiated until every abstract method is overridden.

from abc import ABC, abstractmethod

class JsonMixin:                 # mixin: adds one capability
    def to_json(self): import json; return json.dumps(self.__dict__)

class Shape(ABC):                # abstract base: defines a contract
    @abstractmethod
    def area(self): ...

class Circle(Shape, JsonMixin):
    def __init__(self, r): self.r = r
    def area(self): return 3.14159 * self.r ** 2

Shape()    # TypeError: can't instantiate abstract class

Use mixins to share reusable behaviour and ABCs to enforce that subclasses implement a required interface (isinstance checks also work against ABCs). Rule of thumb: mixins say "you can do this", ABCs say "you must do this".

In cooperative hierarchies, each __init__ should accept **kwargs and pass along what it doesn't consume via super().__init__(**kwargs). This lets every class in the MRO pull out its own arguments without breaking the chain.

class Base:
    def __init__(self, **kwargs):
        super().__init__(**kwargs)      # ends at object()

class Named(Base):
    def __init__(self, name, **kwargs):
        self.name = name
        super().__init__(**kwargs)      # forward the rest

class Aged(Base):
    def __init__(self, age, **kwargs):
        self.age = age
        super().__init__(**kwargs)

class Person(Named, Aged):
    pass
p = Person(name="Ada", age=36)          # both inits run

Rule of thumb: cooperative __init__s take/forward **kwargs and always call super().__init__ so each class consumes its own args and the chain completes.

isinstance respects inheritance — it's True for subclasses too — while type(x) == C demands an exact match and rejects subclasses. isinstance also accepts a tuple of types.

class Animal: pass
class Dog(Animal): pass
d = Dog()

isinstance(d, Animal)        # True — subclass counts
type(d) == Animal            # False — exact type only
isinstance(d, (Dog, Animal)) # True — tuple of options

Rule of thumb: use isinstance for "is this usable as an X?" (the normal case); reserve exact type(x) is C for the rare time you must reject subclasses.

Define the method in the subclass to override it; call super().method() inside to extend rather than fully replace the parent behaviour. This is the standard "do the base thing, then add to it" pattern.

class Logger:
    def log(self, msg):
        print(f"[LOG] {msg}")

class TimestampLogger(Logger):
    def log(self, msg):
        import datetime
        stamp = datetime.datetime.now().isoformat()
        super().log(f"{stamp} {msg}")   # reuse parent, add timestamp

TimestampLogger().log("hi")

Rule of thumb: call super().method() when you want to augment the parent's logic; omit it only when you intend to completely replace it.

Use inheritance for a true "is-a" relationship and shared interface; use composition (holding another object as an attribute) for "has-a" and to avoid fragile deep hierarchies. Composition is more flexible and easier to test.

# Inheritance — Car IS A Vehicle:
class Vehicle: ...
class Car(Vehicle): ...

# Composition — Car HAS AN Engine:
class Engine:
    def start(self): return "vroom"
class Car:
    def __init__(self):
        self.engine = Engine()     # delegate to a part
    def start(self):
        return self.engine.start()

Rule of thumb: reach for composition by default ("has-a", swappable parts); use inheritance when subclasses genuinely substitute for the base ("is-a").

__init_subclass__ is a hook that runs whenever a subclass is defined (not when instantiated). The base class can use it to register subclasses, validate them, or inject defaults — a lighter alternative to a metaclass.

class Plugin:
    registry = []
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Plugin.registry.append(cls)      # auto-register every subclass

class Audio(Plugin): pass
class Video(Plugin): pass
Plugin.registry        # [Audio, Video]

Rule of thumb: use __init_subclass__ for subclass registration/validation; it covers most cases people once reached for metaclasses to do.

Reading instance.attr checks the instance __dict__ first, then walks the class's MRO left to right, returning the first match. So a subclass attribute shadows a base one, and the MRO decides ties in multiple inheritance.

class A:
    x = "A"
class B(A):
    x = "B"
class C(A):
    x = "C"
class D(B, C):
    pass

D.x          # 'B' — first in MRO (D, B, C, A)
d = D(); d.x = "instance"
d.x          # 'instance' — instance dict wins over class

Rule of thumb: lookup order is instance → MRO classes (left to right); check Cls.__mro__ to predict which class attribute a name resolves to.

In Python 3, the zero-argument super() inside a method is sugar for super(CurrentClass, self) — the compiler injects the class and instance. The explicit two-arg form is needed outside a normal method body (e.g. at module level, in some metaclass code, or to start the search from a different class).

class A:
    def f(self): return "A"
class B(A):
    def f(self):
        return super().f()             # = super(B, self).f()

# explicit form, e.g. to skip B in the MRO:
b = B()
super(B, b).f()                         # 'A' — starts after B

Rule of thumb: use bare super() inside methods; the explicit super(Class, obj) form only when there's no enclosing method context or you must control the start.

No — Python has no signature-based overloading. A later def with the same name simply replaces the earlier one. You achieve "overloading" with default args, *args/**kwargs, or functools.singledispatch for type-based dispatch.

class C:
    def f(self, x): return "one"
    def f(self, x, y): return "two"   # this REPLACES the first f

C().f(1)        # TypeError — only the 2-arg f exists

from functools import singledispatchmethod
class D:
    @singledispatchmethod
    def f(self, x): return "default"
    @f.register
    def _(self, x: int): return "int"

Rule of thumb: there's one method per name — use default/variadic args or singledispatch instead of expecting Java-style overloads.

Yes. An intermediate subclass can implement some abstract methods and stay abstract; only a class that implements all of them becomes instantiable. This enables layered hierarchies that share partial implementations.

from abc import ABC, abstractmethod

class Repo(ABC):
    @abstractmethod
    def get(self, id): ...
    @abstractmethod
    def save(self, obj): ...

class ReadOnlyRepo(Repo):
    def get(self, id): return ...        # implements one
    # still abstract: save() missing

ReadOnlyRepo()    # TypeError — save still abstract

Rule of thumb: abstractness propagates until every abstract method is implemented; intermediate classes can fill in part of the contract.

Inside a @classmethod, super() resolves relative to the cls passed in, so it correctly follows the MRO of the actual subclass — making cooperative alternative constructors work across inheritance.

class Base:
    @classmethod
    def create(cls):
        print(f"Base.create for {cls.__name__}")
        return cls()

class Child(Base):
    @classmethod
    def create(cls):
        print("Child.create")
        return super().create()     # cls is still Child

Child.create()    # prints Child.create, then Base.create for Child

Rule of thumb: super() in a classmethod uses the real cls, so factory methods build instances of the correct subclass while still chaining to the base.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel