Inheritance and Polymorphism
Reviewed & published by Brayan K
Create extensible class hierarchies, reuse behavior safely, and design APIs that stay flexible as your codebase grows.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
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 ones# 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
print(f"{self.name} meows")
# Step 3: Use the child classes
dog = Dog("Buddy") # Dog automatically gets __init__ from Animal
cat = Cat("Whiskers") # Cat automatically gets __init__ from Animal
dog.speak() # Output: Buddy barks
cat.speak() # Output: Whiskers meows
# ✅ Expected output:
# Buddy barks
# Whiskers meows✨ 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 ✅ |
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", "Golden Retriever")
print(f"\n{dog.name} is a {dog.breed}") # Both attributes work!
# ✅ Expected output:
# Animal init: Buddy
# Dog init: Golden Retriever
#
# Buddy is a Golden Retriever4️⃣ Method Overriding & Extension Patterns
Override to change behavior; extend to add extra steps:
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)
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 |
# 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()])
# ✅ Expected output:
# Walking
# Swimming
# Quack!
#
# MRO: ['Duck', 'Walker', 'Swimmer', 'object']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 ✅ |
# 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()])
# ✅ Expected output:
# Woof!
# Meow!
# Quack!✨ 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 |
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) -> float: return math.pi * self.r**2 # ✅ Implemented!
# Using polymorphism - all shapes have .area()
shapes = [Rectangle(5, 10), Circle(3)]
for shape in shapes:
print(f"{shape.__class__.__name__}: {shape.area():.2f}")
# ✅ Expected output:
# Rectangle: 50.00
# Circle: 28.279️⃣ 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.
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 = [Square(5), Triangle(4, 6), Square(3)]
print("Total area:", total_area(shapes))
# ✅ Expected output:
# Total area: 46.0This 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 Cat without 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 - 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!
# ✅ Expected output:
# [LOG] ✨ Hello WorldComposition 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.
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", "[email protected]")
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
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), Rectangle(10,1)]
total = sum(s.area() for s in shapes) # polymorphism in action
print("Shapes:", shapes)
print("Total area:", round(total, 2))
# ✅ Expected output:
# Shapes: [Rectangle(3x4), Circle(r=2), Rectangle(10x1)]
# Total area: 34.571️⃣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.
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}")
# ✅ Expected output:
# Vector2(1, 2) + Vector2(3, 4) = Vector2(4, 6)
# Vector2(1, 2) * 3 = Vector2(3, 6)1️⃣6️⃣ Testing Polymorphism (Strategy Pattern Feel)
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)
# ✅ Expected output:
# Paid £9.99 cash
# Charged £9.99 to card
# Sent £9.99 in cryptoNo 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())
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()])
# ✅ Expected output:
# a=1, b=2, base=True
# MRO: ['C', 'A', 'B', 'Base', 'object']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 Shape with area().
- Implement Rectangle(w,h) and Circle(r).
- Build a list of shapes, then compute total area with polymorphism.
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:
return sum(s.area() for s in items)
shapes = [Rectangle(3,4), Circle(5), Rectangle(10,1)]
print("Shapes:", shapes)
print("Total area:", round(total_area(shapes), 2))
# ✅ Expected output:
# Shapes: [Rectangle(3x4), Circle(r=5), Rectangle(10x1)]
# Total area: 100.54📋 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.
Practice quiz
How do you make class Dog inherit from class Animal?
- class Dog -> Animal:
- class Dog inherits Animal:
- class Dog(Animal):
- class Dog extends Animal:
Answer: class Dog(Animal):. Python uses parentheses: class Dog(Animal): puts the parent class inside the parentheses.
Inside a child's __init__, how do you call the parent class's __init__?
- super().__init__(name)
- parent.__init__(name)
- Animal.init(name)
- self.super(name)
Answer: super().__init__(name). super().__init__(name) runs the parent constructor so its setup (like self.name) actually happens.
What does Duck.mro() return for class Duck(Walker, Swimmer)?
- ['Walker', 'Swimmer', 'Duck']
- ['object', 'Duck']
- ['Duck', 'Swimmer', 'Walker', 'object']
- ['Duck', 'Walker', 'Swimmer', 'object']
Answer: ['Duck', 'Walker', 'Swimmer', 'object']. The MRO lists Duck first, then parents left-to-right (Walker, Swimmer), then object.
What does the @abstractmethod decorator (from abc) enforce?
- The method runs automatically
- Child classes must implement that method
- The method becomes static
- The method is cached
Answer: Child classes must implement that method. An abstractmethod is a promise: any concrete subclass must implement it, or instantiation fails.
What is 'duck typing' in Python?
- Caring only about whether an object has the needed method, not its type
- Requiring all objects to share a parent class
- A way to copy ducks
- Strict compile-time type checking
Answer: Caring only about whether an object has the needed method, not its type. Duck typing: if it has .speak(), you can call it — Python checks behavior, not the exact type.
Given Vector2 with __add__ defined, what does Vector2(1, 2) + Vector2(3, 4) produce?
- Vector2(3, 8)
- Vector2(1, 2, 3, 4)
- Vector2(4, 6)
- a TypeError
Answer: Vector2(4, 6). __add__ adds component-wise: x=1+3=4, y=2+4=6, giving Vector2(4, 6).
An abstract base class created with abc.ABC and an abstractmethod...
- can be instantiated directly
- cannot be instantiated directly
- has no methods
- must inherit from object explicitly
Answer: cannot be instantiated directly. Abstract classes act as templates/contracts and cannot be instantiated directly.
Which relationship signals you should prefer composition over inheritance?
- IS-A (Dog is an Animal)
- Any 2-level hierarchy
- Polymorphic methods
- HAS-A (Car has an Engine)
Answer: HAS-A (Car has an Engine). A HAS-A relationship (a Car has an Engine) is best modeled with composition, not inheritance.
The Liskov Substitution Principle (LSP) says a subclass should...
- always add new attributes
- be usable anywhere its parent is expected, without surprises
- never override methods
- require stricter inputs than the parent
Answer: be usable anywhere its parent is expected, without surprises. LSP: code that works with the parent type should work with any subclass without breaking expectations.
What is a mixin?
- A class that cannot be subclassed
- A function decorator
- A small parent that adds focused behavior, with no standalone identity
- A way to merge two instances
Answer: A small parent that adds focused behavior, with no standalone identity. A mixin is a small, behavior-only parent (named *Mixin) you mix in to add reusable functionality.
Continue this course
- Previous: Object-Oriented Programming
- Next: Decorators & Advanced Features — Wrap functions to add behaviour without modifying their code
- Quick reference: Python cheat sheet › Classes
- From the blog: Object-Oriented Programming in Python