Lesson 13 • Expert
Inheritance & Polymorphism (Expert) 🧬
Create extensible class hierarchies, reuse behavior safely, and design APIs that stay flexible as your codebase grows.
What You'll Learn in This Lesson
- • How subclasses inherit methods and attributes from parent classes
- • Overriding methods and calling
super() - • Method Resolution Order (MRO) in Python
- • Abstract base classes with
abc.ABC - • Polymorphism: writing code that works on multiple types
1️⃣ Why Inheritance? Why Polymorphism?
🏠 Real-World Analogy:
Think of a family tree. Children inherit traits from their parents (eye color, height) but can also have their own unique traits. In programming, a child class inherits features from a parent class but can add or change behaviors.
| Concept | What It Means | Example |
|---|---|---|
| Inheritance | A child class reuses and extends a parent class | Dog inherits from Animal |
| Polymorphism | "Many forms" - same method name, different behaviors | .speak() returns "Woof!" or "Meow!" |
Polymorphism means "many forms": different classes expose the same method name but implement it differently, so your high-level code doesn't care which concrete type it's using—only that it supports the interface.
💡 Why This Matters: This improves extensibility, testability, and readability in real projects like games, APIs, and data pipelines.
2️⃣ Core Inheritance Syntax
📝 The Basic Pattern:
class ParentClass: # The "base" or "parent" class
# shared attributes and methods
class ChildClass(ParentClass): # ← Inherits from ParentClass
# can add new features or override existing onesCore Inheritance
Basic parent-child class relationship
# Step 1: Create the Parent (Base) class
class Animal:
def __init__(self, name):
self.name = name # All animals have a name
def speak(self):
print(f"{self.name} makes a sound")
# Step 2: Create Child (Derived) classes that inherit from Animal
class Dog(Animal): # Dog inherits from Animal
def speak(self): # Override the speak method
print(f"{self.name} barks")
class Cat(Animal): # Cat inherits from Animal
def speak(self): # Override the speak method
...✨ Key Point: The child inherits ALL attributes and methods from the parent. You can then override (replace) or extend (add to) them.
3️⃣ super() and Constructor Chaining
🤔 What is super()?
super() is a special function that gives you access to the parent class. It's like saying "Hey, parent class, please run YOUR version of this method first!"
| Without super() | With super() |
|---|---|
Parent's __init__ is NOT called | Parent's __init__ IS called ✅ |
| Parent's attributes are missing | All parent attributes are set up properly ✅ |
super() Function
Call parent class methods
class Animal:
def __init__(self, name):
self.name = name # Parent sets up 'name'
print(f"Animal init: {name}")
class Dog(Animal):
def __init__(self, name, breed):
# Step 1: Call parent's __init__ FIRST
super().__init__(name) # ← This runs Animal.__init__
# Step 2: Then add child-specific attributes
self.breed = breed
print(f"Dog init: {breed}")
# When we create a Dog, BOTH __init__ methods run!
dog = Dog("Buddy", "Gold
...super().__init__() means the parent's setup code never runs, and attributes like self.name won't exist!4️⃣ Method Overriding & Extension Patterns
Override to change behavior; extend to add extra steps:
Method Override & Extend
Customize parent behavior
from datetime import datetime
class Logger:
def log(self, msg):
print(f"[LOG] {msg}")
class TimedLogger(Logger):
def log(self, msg):
timestamp = datetime.now().strftime("%H:%M:%S")
super().log(f"{timestamp} — {msg}") # extend
logger = TimedLogger()
logger.log("Application started")
logger.log("User logged in")5️⃣ Multiple Inheritance & MRO (Method Resolution Order)
🏠 Real-World Analogy:
A smartphone inherits features from both a phone AND a camera. That's multiple inheritance—one class getting abilities from multiple parents!
| Term | What It Means |
|---|---|
| Multiple Inheritance | A class inherits from 2+ parent classes |
| MRO | Method Resolution Order—the order Python checks classes for a method |
Multiple Inheritance
Inherit from multiple classes
# Two separate parent classes
class Walker:
def walk(self): print("Walking")
class Swimmer:
def swim(self): print("Swimming")
# Duck inherits from BOTH Walker AND Swimmer!
class Duck(Walker, Swimmer): # ← Multiple inheritance
def quack(self): print("Quack!")
# Duck can do everything!
d = Duck()
d.walk() # From Walker
d.swim() # From Swimmer
d.quack() # Its own method
# See the order Python checks for methods:
print("\nMRO:", [c.__name__ for c in Duck.mro()])6️⃣ When to Use Inheritance (and When Not)
✅ Use when:
- • Clear IS-A relationship: Dog is a Animal, Car is a Vehicle.
- • You truly want to reuse base logic and maybe override small parts.
❌ Avoid when:
- • Relationship is HAS-A (composition is better): A Car has an Engine.
- • You're forcing a deep chain just to share helpers (prefer composition or mixins).
Rule of thumb: Favor composition over inheritance unless the IS-A relation is obvious and stable.
7️⃣ Polymorphism via Duck Typing
🦆 What is Duck Typing?
"If it walks like a duck and quacks like a duck, it's a duck!"
In Python, we don't care what type an object is—we only care what it can do. If it has a .speak() method, we can call it!
| Traditional OOP | Python Duck Typing |
|---|---|
| Must inherit from same parent | No inheritance required! ✅ |
| Strict type checking | Just needs the right methods ✅ |
Duck Typing
If it quacks like a duck...
# These classes have NO common parent!
class Dog:
def speak(self): return "Woof!"
class Cat:
def speak(self): return "Meow!"
class Duck:
def speak(self): return "Quack!"
# But they ALL have .speak() - that's all we need!
def chorus(animals):
for a in animals:
print(a.speak()) # Works because they all have .speak()
# Python doesn't check types - it just calls the method
chorus([Dog(), Cat(), Duck()])✨ Key Insight: This is duck typing in action! Python is flexible—it doesn't need formal interfaces or shared parents.
8️⃣ Abstract Base Classes (ABCs) for Robust APIs
🤔 What is an Abstract Class?
An abstract class is like a template or contract. It says "any class that inherits from me MUST implement these methods" but doesn't provide the implementation itself.
| Regular Class | Abstract Class |
|---|---|
| Can be instantiated directly | Cannot be instantiated ❌ |
| All methods have implementations | Some methods are just "promises" |
| Child classes can override optionally | Child classes MUST implement abstract methods |
Abstract Base Classes
Define interface contracts
from abc import ABC, abstractmethod
import math
# Abstract class - can't be instantiated directly
class Shape(ABC):
@abstractmethod # ← This decorator means "child MUST implement this"
def area(self) -> float: ...
# Concrete classes - MUST implement area()
class Rectangle(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self) -> float: return self.w * self.h # ✅ Implemented!
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self) -> floa
...area(), Python raises an error immediately—great for catching bugs early!9️⃣ Protocols (typing) for Structural Polymorphism
With type hints you can express "any object with .area() is acceptable"—even if it doesn't inherit from Shape.
Protocols
Structural subtyping
from typing import Protocol
class AreaLike(Protocol):
def area(self) -> float: ...
def total_area(shapes: list) -> float:
return sum(s.area() for s in shapes)
# Any class with .area() works, no inheritance needed!
class Square:
def __init__(self, side): self.side = side
def area(self): return self.side ** 2
class Triangle:
def __init__(self, base, height):
self.base, self.height = base, height
def area(self): return 0.5 * self.base * self.height
shapes = [Sq
...This is powerful for plug-in systems and testing.
🔟 Liskov Substitution Principle (LSP)
🏠 Simple Explanation:
If you have code that works with Animal, it should also work with Dog or Catwithout any surprises. A child class should behave like its parent, not break expectations.
| ✅ Good (Follows LSP) | ❌ Bad (Violates LSP) |
|---|---|
Bird.fly() → flies as expected | Penguin.fly() → raises error! |
Rectangle.area() → returns area | Square.area() → changes width when you set height?! |
⚠️ Signs You're Violating LSP:
- • Child requires stricter inputs than parent
- • Child returns different types than parent promised
- • Child throws errors the parent never would
💡 Rule of Thumb: If you need to check "is this a Dog or a Cat?" before calling a method, you might be violating LSP. Good polymorphism means you don't need to check!
1️⃣1️⃣ Composition vs Inheritance — Concrete Example
Composition over Inheritance
HAS-A vs IS-A relationships
# ✅ Composition - HAS-A relationship
class Logger:
def log(self, msg):
print(f"[LOG] {msg}")
class FancyFileLogger:
def __init__(self, logger):
self.logger = logger # HAS-A
def log(self, msg):
self.logger.log(f"✨ {msg}")
base_logger = Logger()
fancy = FancyFileLogger(base_logger)
fancy.log("Hello World")
# Composition is easier to refactor and test!Composition is easier to refactor and avoids MRO tangles.
1️⃣2️⃣ Mixins: Small, Focused Behavior Blocks
A mixin is a parent with only helper behavior, no standalone identity.
Mixins
Reusable behavior blocks
import json
class JSONSerializableMixin:
def to_json(self):
return json.dumps(self.__dict__)
class TimestampMixin:
def get_timestamp(self):
from datetime import datetime
return datetime.now().isoformat()
class User(JSONSerializableMixin, TimestampMixin):
def __init__(self, name, email):
self.name = name
self.email = email
user = User("Boopie", "boopie@example.com")
print(user.to_json())
print(user.get_timestamp())Use mixins sparingly, name them *Mixin, and avoid shared state.
1️⃣3️⃣ Real-World Hierarchy: Shapes & Polymorphic Area
Polymorphic Shapes
Real-world inheritance example
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Rectangle(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h
def __repr__(self): return f"Rectangle({self.w}x{self.h})"
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return math.pi * self.r**2
def __repr__(self): return f"Circle(r={self.r})"
shapes = [Rectangle(3,4), Circle(2),
...1️⃣4️⃣ Overriding Pitfalls & Best Practices
- ✅Always call
super()in__init__if parent defines it (helps multiple inheritance). - ✅Keep overrides behaviorally compatible with the base.
- ✅Document side effects and invariants (e.g., "area() returns non-negative float").
- ✅Unit-test with base type references to catch LSP issues.
1️⃣5️⃣ Polymorphism Beyond Methods: Operators & Special Methods
Special methods let your classes participate in Python's operators—another form of polymorphism.
Operator Overloading
Custom operators for classes
class Vector2:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vector2(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector2(self.x * scalar, self.y * scalar)
def __repr__(self):
return f"Vector2({self.x}, {self.y})"
v1 = Vector2(1, 2)
v2 = Vector2(3, 4)
print(f"{v1} + {v2} = {v1 + v2}")
print(f"{v1} * 3 = {v1 * 3}")1️⃣6️⃣ Testing Polymorphism (Strategy Pattern Feel)
Strategy Pattern
Polymorphic payment methods
class CashPayment:
def pay(self, amount): print(f"Paid £{amount} cash")
class CardPayment:
def pay(self, amount): print(f"Charged £{amount} to card")
class CryptoPayment:
def pay(self, amount): print(f"Sent £{amount} in crypto")
def checkout(amount, method):
method.pay(amount) # any object with .pay works
for m in (CashPayment(), CardPayment(), CryptoPayment()):
checkout(9.99, m)No inheritance required, but you still get clean polymorphism. If you later need guarantees, move to an ABC/Protocol.
1️⃣7️⃣ Multiple Inheritance Done Right (Cooperative super())
Cooperative super()
Safe multiple inheritance
class Base:
def __init__(self, **kw):
super().__init__(**kw)
self.base = True
class A(Base):
def __init__(self, a, **kw):
super().__init__(**kw)
self.a = a
class B(Base):
def __init__(self, b, **kw):
super().__init__(**kw)
self.b = b
class C(A, B):
def __init__(self, a, b):
super().__init__(a=a, b=b)
c = C(1, 2)
print(f"a={c.a}, b={c.b}, base={c.base}")
print("MRO:", [cls.__name__ for cls in C.mro()])All classes call super() with **kw, so init flows through the MRO smoothly.
1️⃣8️⃣ Performance & Practicality
- ⚡Virtual dispatch (method lookup) is fast enough for most apps.
- 📐Prefer small, stable bases with clear contracts over deep trees.
- 🧹Keep methods short; big conditionals might indicate a missing subtype.
🎯 1️⃣9️⃣ Practice Challenge (with Guided Solution)
Task:
- Create an abstract
Shapewitharea(). - Implement
Rectangle(w,h)andCircle(r). - Build a list of shapes, then compute total area with polymorphism.
Practice Challenge
Build a shape hierarchy
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Rectangle(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h
def __repr__(self): return f"Rectangle({self.w}x{self.h})"
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return math.pi * self.r**2
def __repr__(self): return f"Circle(r={self.r})"
def total_area(items: list) -> float
...📋 Quick Reference — Inheritance
| class Dog(Animal): | Inherit from Animal |
| super().__init__(name) | Call parent constructor |
| def speak(self): | Override a parent method |
| Dog.mro() | See Method Resolution Order |
| from abc import ABC, abstractmethod | Abstract base class |
🎉 Great work! You've completed this lesson.
You now understand inheritance, method overriding, super(), MRO, and polymorphism — the tools that make Python class hierarchies clean and extensible.
Up next: Decorators & Advanced Features — learn to wrap functions with reusable behaviour using Python's elegant decorator syntax.
Sign up for free to track which lessons you've completed and get learning reminders.