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

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.

ConceptWhat It MeansExample
InheritanceA child class reuses and extends a parent classDog 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 calledParent's __init__ IS called ✅
Parent's attributes are missingAll 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 Retriever

4️⃣ 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!

TermWhat It Means
Multiple InheritanceA class inherits from 2+ parent classes
MROMethod 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:

❌ Avoid when:

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 OOPPython Duck Typing
Must inherit from same parentNo inheritance required! ✅
Strict type checkingJust 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 ClassAbstract Class
Can be instantiated directlyCannot be instantiated ❌
All methods have implementationsSome methods are just "promises"
Child classes can override optionallyChild 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.27

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.

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.0

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 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 expectedPenguin.fly() → raises error!
Rectangle.area() → returns areaSquare.area() → changes width when you set height?!

⚠️ Signs You're Violating LSP:

💡 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 World

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.

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.57

1️⃣4️⃣ Overriding Pitfalls & Best Practices

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 crypto

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())

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

🎯 1️⃣9️⃣ Practice Challenge (with Guided Solution)

Task:

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, abstractmethodAbstract 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