Courses/Python/Inheritance & Polymorphism

    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.

    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

    Core Inheritance

    Basic parent-child class relationship

    Try it Yourself »
    Python
    # 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 calledParent's __init__ IS called ✅
    Parent's attributes are missingAll parent attributes are set up properly ✅

    super() Function

    Call parent class methods

    Try it Yourself »
    Python
    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
    ...

    4️⃣ Method Overriding & Extension Patterns

    Override to change behavior; extend to add extra steps:

    Method Override & Extend

    Customize parent behavior

    Try it Yourself »
    Python
    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!

    TermWhat It Means
    Multiple InheritanceA class inherits from 2+ parent classes
    MROMethod Resolution Order—the order Python checks classes for a method

    Multiple Inheritance

    Inherit from multiple classes

    Try it Yourself »
    Python
    # 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 OOPPython Duck Typing
    Must inherit from same parentNo inheritance required! ✅
    Strict type checkingJust needs the right methods ✅

    Duck Typing

    If it quacks like a duck...

    Try it Yourself »
    Python
    # 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 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

    Abstract Base Classes

    Define interface contracts

    Try it Yourself »
    Python
    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
    ...

    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

    Try it Yourself »
    Python
    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 expectedPenguin.fly() → raises error!
    Rectangle.area() → returns areaSquare.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

    Try it Yourself »
    Python
    # ✅ 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

    Try it Yourself »
    Python
    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

    Try it Yourself »
    Python
    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

    Try it Yourself »
    Python
    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

    Try it Yourself »
    Python
    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

    Try it Yourself »
    Python
    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:

    1. Create an abstract Shape with area().
    2. Implement Rectangle(w,h) and Circle(r).
    3. Build a list of shapes, then compute total area with polymorphism.

    Practice Challenge

    Build a shape hierarchy

    Try it Yourself »
    Python
    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, 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.

    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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service