Engineering Notes
Home/Languages/Python/Inheritance and Polymorphism
Languages · Python

Inheritance and Polymorphism

Dog(Animal) means Dog inherits all of Animal's attributes and methods, overriding whatever it defines itself. Calling .speak() on any Animal subclass dispatches to that specific subclass's implementation — this is polymorphism: one interface, many concrete behaviors, chosen at runtime based on the actual object's type.

💻 Languages● Intermediate⏱ 5 min read🛠️ 3 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Basic Inheritance
  • super() — Calling the Parent's Implementation
  • Overriding and Extending Methods
  • Abstract Base Classes
  • Multiple Inheritance and the Diamond Problem

1Basic Inheritance

python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError("subclasses must implement speak()")

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow"

for animal in [Dog("Rex"), Cat("Whiskers")]:
    print(animal.speak())  # polymorphism: same call, different behavior per subclass

Dog(Animal) means Dog inherits all of Animal's attributes and methods, overriding whatever it defines itself. Calling .speak() on any Animal subclass dispatches to that specific subclass's implementation — this is polymorphism: one interface, many concrete behaviors, chosen at runtime based on the actual object's type.

2super() — Calling the Parent's Implementation

python
class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)   # delegates to Animal.__init__, don't repeat its logic
        self.breed = breed

d = Dog("Rex", "Labrador")

super() gives you a proxy for calling the next class in the method resolution order (MRO) — usually the immediate parent, but not always in multiple inheritance (section 5). Using super().__init__(...) instead of Animal.__init__(self, ...) matters because it respects the MRO, which becomes essential once multiple inheritance is involved.

3Overriding and Extending Methods

python
class Vehicle:
    def describe(self):
        return "A vehicle"

class Car(Vehicle):
    def describe(self):
        base = super().describe()   # extend, don't just replace
        return f"{base} that is a car"

print(Car().describe())  # "A vehicle that is a car"

Overriding entirely replaces a method; calling super().method() inside the override lets you extend the parent's behavior instead of duplicating it.

4Abstract Base Classes

python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        ...

class Square(Shape):
    def __init__(self, side):
        self.side = side
    def area(self):
        return self.side ** 2

Shape()          # TypeError — can't instantiate an abstract class
Square(4).area()  # 16

ABC + @abstractmethod enforces, at instantiation time, that concrete subclasses actually implement every abstract method — a stronger contract than just raising NotImplementedError in the base method body, which only fails if/when that specific method is actually called.

5Multiple Inheritance and the Diamond Problem

python
class A:
    def greet(self):
        return "A"

class B(A):
    def greet(self):
        return "B->" + super().greet()

class C(A):
    def greet(self):
        return "C->" + super().greet()

class D(B, C):
    pass

print(D().greet())  # "B->C->A" — NOT "B->A", thanks to cooperative MRO
print(D.__mro__)     # (D, B, C, A, object)

Naively, you might expect super() inside B to jump straight to A, skipping C entirely. It doesn't — super() follows the class's linearized MRO, not "my direct parent," which is exactly what lets a diamond hierarchy call every class exactly once, in a consistent order (full mechanics in section 6).

Under the HoodC3 Linearization and Cooperative super()▾

The MRO is computed once, at class-creation time, by the C3 algorithm

When a class statement with bases executes, CPython computes a full, fixed ordering of every ancestor class — the Method Resolution Order — using the C3 linearization algorithm, and stores it as __mro__ on the resulting type object. C3 guarantees two properties: a class always appears before its parents (local precedence), and the relative order of appearance of the bases themselves in a class's declaration is preserved across the whole hierarchy (monotonicity). If no consistent order can satisfy both constraints (a genuinely contradictory hierarchy), class creation fails immediately with TypeError: Cannot create a consistent method resolution order.

python
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

print(D.__mro__)
# (D, B, C, A, object) — computed once when `class D(B, C):` executes

super() is not "my parent" — it's "the next class after me in the CALLER's MRO"

This is the single most misunderstood mechanic in Python inheritance. super() inside a method defined on class X, called on an instance of (possibly a further subclass) Y, looks up Y.__mro__, finds X's position in that list, and dispatches to whatever comes immediately after X in Y's MRO — not necessarily X's own direct base.

python
# from section 5: D.__mro__ = (D, B, C, A, object)
# B.greet() calls super().greet() — but self is a D instance, so
# super() consults D's MRO, finds B, and calls the NEXT entry: C.
# C.greet() calls super().greet() — same D.__mro__, finds C, calls A.
super() inside B.greet(), called on a D instance, walks D's MRO (not B's own base) to find C next, then A
super() inside B.greet(), called on a D instance, walks D's MRO (not B's own base) to find C next, then A

This is exactly how every class in a diamond gets visited exactly once despite each one only knowing about its own direct declared bases — the instance's full MRO is what every super() call in the chain actually consults, which is why the pattern only works reliably when every class in the hierarchy cooperatively calls super() instead of hardcoding a parent class name.

isinstance/issubclass walk the MRO too

python
isinstance(D(), A)   # True — A is in D's MRO
issubclass(D, A)      # True — same check, class-to-class

Both built-ins are, mechanically, membership checks against __mro__ (with additional support for __subclasshook__/ABC virtual subclassing, e.g. how collections.abc.Sequence recognizes list without list literally inheriting from it).

⚠️ Common Pitfalls

  1. Hardcoding the parent class name instead of super() — Animal.__init__(self, name) works for simple single inheritance but breaks cooperative multiple inheritance, since it bypasses the MRO entirely and can call a class twice or skip one.
  2. Assuming super() means "my direct parent" — as shown in section 6, it means "the next class in the actual instance's MRO after me" — these differ under multiple inheritance.
  3. Diamond hierarchies where not every class calls super() — if any class in the chain skips the super().method() call, the chain breaks and classes after it never get invoked.
  4. Catching TypeError about inconsistent MRO too late — it happens at class-definition time (when the class statement runs), not when you instantiate — design multiple-inheritance hierarchies carefully, don't discover the conflict at first use.

📋 Quick Reference

python
class Dog(Animal): ...             # single inheritance
class D(B, C): ...                   # multiple inheritance — MRO computed via C3
super().__init__(...)                 # delegates to next class in the INSTANCE's MRO
D.__mro__                              # tuple showing the full resolution order
isinstance(obj, Cls)                    # True if type(obj) or any ancestor is Cls
issubclass(Sub, Base)                    # True if Base is in Sub's MRO
from abc import ABC, abstractmethod       # enforce subclasses implement required methods

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1How does inheritance and super() work?core▾

A class can extend one or more base classes; super() calls the next class in the MRO (method resolution order, C3 linearization), which matters with multiple inheritance.

Follow-ups interviewers ask
  • See the MRO? Class.__mro__.
Q2What is duck typing?core▾

Python cares about behaviour, not type: if an object has the needed methods (read()), it works. Protocols (typing.Protocol) express this for static type checkers.

Follow-ups interviewers ask
  • EAFP? Easier to ask forgiveness than permission: try and catch exceptions.
Q3What are abstract base classes?core▾

abc.ABC with @abstractmethod defines interfaces that subclasses must implement; instantiating an incomplete subclass raises TypeError.

Follow-ups interviewers ask
  • ABC vs Protocol? ABCs need explicit inheritance; Protocols are structural.
Q4What are mixins?senior▾

Small classes providing extra behaviour (e.g. JsonMixin) combined via multiple inheritance; they should be focused and avoid their own state where possible.

Follow-ups interviewers ask
  • Risk? Complex MROs and hidden coupling.
Q5Composition vs inheritance in Python?core▾

Prefer composition for flexibility (objects holding collaborators), inheritance for true is-a relationships or framework extension points. Deep hierarchies are hard to change.

Follow-ups interviewers ask
  • Pythonic alternative? Functions and protocols over class hierarchies.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.