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.
🎯 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
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 subclassDog(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
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
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
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() # 16ABC + @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
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.
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):` executessuper() 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.
# 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.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
isinstance(D(), A) # True — A is in D's MRO
issubclass(D, A) # True — same check, class-to-classBoth 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
- 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. - 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. - Diamond hierarchies where not every class calls
super()— if any class in the chain skips thesuper().method()call, the chain breaks and classes after it never get invoked. - Catching
TypeErrorabout inconsistent MRO too late — it happens at class-definition time (when theclassstatement runs), not when you instantiate — design multiple-inheritance hierarchies carefully, don't discover the conflict at first use.
📋 Quick Reference
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
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.
- 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.
- 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.
- 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.
- 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.
- 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.