Inheritance is a mechanism by which a class (subclass) acquires the attributes and methods of another class (superclass), enabling code reuse through is-a relationships and allowing method overriding to specialise inherited behaviour.
Building on existing classes
What you'll learn: The is-a relationship that justifies inheritance. How a subclass inherits attributes and methods from a parent. The super() function. When NOT to use inheritance.
How to read this tab: Read alongside Object-Oriented Python which covers Python-specific syntax in detail.
Inheritance lets a new class start with everything an existing class already has, then add or change only what is different — write shared behaviour once, reuse it everywhere.
Inheritance is only appropriate for genuine is-a relationships. Dog IS-A Animal: ✓ inherit. Car HAS-A Engine: ✗ don't inherit, compose. The test: would it make sense to use a Dog everywhere an Animal is expected? Yes → inherit. The most common misuse of inheritance is using it for code reuse when composition would be cleaner.
The is-a relationship
A SavingsAccount is a BankAccount. A Dog is an Animal. The child class (subclass) inherits all attributes and methods of the parent class (superclass) and can add new ones or override existing ones.
class Animal:
def __init__(self, name): self.name = name
def speak(self): return f"{self.name} makes a sound"
class Dog(Animal):
def speak(self): # override
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
rex = Dog("Rex")
whiskers = Cat("Whiskers")
print(rex.speak()) # Rex says Woof!
print(whiskers.speak()) # Whiskers says Meow!
print(isinstance(rex, Animal)) # TrueAlways use super() — never call the parent directly by name. Instead of Animal.__init__(self) write super().__init__(). Why: super() respects the MRO — in multiple inheritance, it ensures each parent is called exactly once in the right order. Calling by name breaks this in complex hierarchies.
super(): calling the parent
super() gives access to the parent's methods without naming it explicitly. Essential in __init__: the subclass must call the parent's init to ensure parent attributes are set up.
class SavingsAccount(BankAccount):
def __init__(self, owner, balance=0, rate=0.04):
super().__init__(owner, balance) # call parent __init__
self.rate = rate
def add_interest(self):
interest = self.balance * self.rate
self.deposit(interest) # inherited method
return interestPrefer composition over inheritance. Inheritance creates tight coupling between classes — a change in the parent can break all subclasses. Composition (storing an object as an attribute) is more flexible. The rule: if you need the subclass to be usable as the parent type, use inheritance. If you just want to reuse the parent's code, use composition.
When NOT to use inheritance
Use inheritance only for genuine is-a relationships. For code reuse without an is-a relationship, composition is better: give a class an instance of another class (has-a). The Gang of Four (Gamma et al., 1994) advise: favour composition over inheritance.
✅ Beginner tab complete — check your understanding
- I can explain inheritance using an is-a relationship (Dog IS-A Animal)
- I can write a subclass that inherits from a parent and adds new methods
- I know when NOT to use inheritance: when composition (has-a) is cleaner
- I can use super() to call the parent class's method from the subclass
Multiple inheritance, MRO, and abstract classes
What you'll learn: Multiple inheritance — inheriting from more than one class. The C3 Method Resolution Order that determines which parent's method is called. Abstract base classes for defining interfaces.
How to read this tab: The MRO section is directly relevant to real Python code. Call type.mro() on any class to see the order — try it on list, dict, and your own classes.
Python's C3 linearisation resolves multiple inheritance correctly. If class C inherits from both A and B, and both have a method foo(), Python uses the MRO to decide which foo() to call. The order is computed by C3 linearisation — a deterministic algorithm that respects both the local order of bases and the global inheritance hierarchy.
Multiple inheritance and the MRO
Python supports class C(A, B). Python's C3 linearisation resolves method lookup consistently. C.__mro__ shows the lookup order. Java and Kotlin disallow multiple class inheritance (diamond problem prevention) but allow multiple interface implementation.
class A:
def hello(self): return "A"
class B(A):
def hello(self): return "B"
class C(A):
def hello(self): return "C"
class D(B, C): pass
print(D.__mro__)
# D, B, C, A, object
print(D().hello()) # "B" — MRO says check D, B, C, A in orderAbstract classes define an interface without an implementation. Use from abc import ABC, abstractmethod. Mark methods with @abstractmethod to force subclasses to implement them. Instantiating an abstract class raises TypeError. This is how Python enforces that a subclass provides required methods — closest Python gets to Java's interface.
Abstract classes
An abstract class cannot be instantiated — it exists to be subclassed. Abstract methods must be implemented by concrete subclasses. Python: from abc import ABC, abstractmethod. Java: abstract class. The compiler/runtime enforces that all abstract methods are implemented.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14159 * self.r ** 2
# Shape() raises TypeError — can't instantiate abstract
c = Circle(5)
print(c.area()) # 78.54The formal rule for correct inheritance. A subclass must be substitutable for its parent without breaking the program. If your Dog class inherits from Animal but breaks when used where Animal is expected (wrong return type, narrower accepted inputs, unexpected exceptions), it violates LSP. LSP violations indicate the inheritance relationship is wrong — usually composition would be better.
Liskov Substitution Principle
LSP (Liskov & Wing, 1994): a subclass must be usable wherever its parent is expected without breaking the program. Classic violation: Square overriding Rectangle's set_width/set_height breaks code that sets width and height independently.
✅ Intermediate tab complete — check your understanding
- I can use super() correctly in a multiple inheritance hierarchy
- I can call type.mro() or ClassName.__mro__ to see the resolution order
- I know what an abstract class is and how to define one with ABC and @abstractmethod
LSP, the diamond problem, covariance and contravariance
What you'll learn: The Liskov Substitution Principle — the formal rule for when inheritance is correct. The diamond problem and how C3 linearisation solves it. Covariance and contravariance in type systems.
How to read this tab: Read when studying software design principles or type theory.
The diamond problem and C3 linearisation
With class D(B, C) where both B and C inherit from A: which path to A's method does D use? C3 linearisation (Barrett et al., 1996) ensures A's method is called exactly once and produces a total ordering: L(D) = D + merge(L(B), L(C), [B, C]). Merge repeatedly takes the head of the first list if it does not appear in the tail of any other list.
Covariance and contravariance
Type-safe overriding requires careful treatment. Covariant return type: an override can return a more specific type (safe — callers expecting T get S⊂T). Contravariant parameter type: an override can accept a less specific type (safe — handles more cases). Java supports covariant return types; parameter types are invariant. Java array types are covariant — a historical mistake that enables ArrayStoreException at runtime.
Liskov, B. & Wing, J. (1994). ACM TOPLAS 16(6). Barrett, D. et al. (1996). OOPSLA '96. Python abc module: docs.python.org/3/library/abc.html. Java Language Specification §8.4.8 — Overriding.
✅ Expert tab complete
- I can explain the Liskov Substitution Principle (a subclass must be usable wherever the parent is expected)
- I can explain the diamond problem and how MRO resolves it