🏗️ Phase 3 · OOP 🟡 Intermediate MODULE 12

Inheritance & Polymorphism

⏱️ 40 min
📖 Theory + Code
🧩 5 Questions
🏗️ 1 Challenge
Phase 3 progress33%
🎯 What you'll learn: How child classes inherit from parents, how to use super() to extend (not replace) parent behaviour, method overriding, multiple inheritance and the MRO, the isinstance() / issubclass() checks, Abstract Base Classes with ABC, and how polymorphism lets you write one function that works for many types.

Basic Inheritance

Inheritance lets a child class automatically get all the attributes and methods of a parent class. The child can then add new methods or override existing ones. This is the "is-a" relationship: a Dog is a Animal.

Animal
Dog
Cat
Bird
inheritance_basics.py
PYTHON
# Parent class (base class)
class Animal:
    def __init__(self, name, species):
        self.name    = name
        self.species = species
        self.energy  = 100

    def eat(self, food):
        self.energy += 10
        return f"{self.name} eats {food}. Energy: {self.energy}"

    def speak(self):
        return "..."

    def __str__(self):
        return f"{self.name} ({self.species})"


# Child class — inherits everything from Animal
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, "Canis lupus")  # call parent __init__
        self.breed = breed   # dog-specific attribute

    # Override parent method
    def speak(self):
        return f"{self.name} says: Woof! 🐶"

    # New method — only on Dog
    def fetch(self, item):
        self.energy -= 15
        return f"{self.name} fetches the {item}! Energy: {self.energy}"


class Cat(Animal):
    def __init__(self, name, indoor=True):
        super().__init__(name, "Felis catus")
        self.indoor = indoor

    def speak(self):
        return f"{self.name} says: Meow! 🐱"


dog = Dog("Rex", "German Shepherd")
cat = Cat("Luna")

print(dog)                   # Rex (Canis lupus) — uses Animal.__str__
print(dog.speak())           # Rex says: Woof! 🐶
print(dog.eat("kibble"))    # inherited from Animal
print(dog.fetch("ball"))    # Dog-only method
print(cat.speak())           # Luna says: Meow! 🐱

# isinstance checks
print(isinstance(dog, Dog))    # True
print(isinstance(dog, Animal)) # True  — Dog IS AN Animal
print(isinstance(cat, Dog))    # False
💡
super() calls the parent class — always use it in __init__
super().__init__(...) runs the parent's constructor so you don't have to duplicate code. Without it, the parent's __init__ never runs and the parent attributes are never set. In Python 3, super() with no arguments automatically finds the correct parent.

Polymorphism in Action

Polymorphism means "many forms." In Python, it means you can write a function that accepts any object and calls a method on it — and each object responds in its own way. This is Python's "duck typing": if it has a speak() method, you can call speak() on it, regardless of its type.

polymorphism.py
PYTHON
# Polymorphism — one function, many types
def animal_concert(animals):
    """Make every animal speak — works for any Animal subclass."""
    for animal in animals:
        print(animal.speak())   # each responds differently

class Bird(Animal):
    def __init__(self, name):
        super().__init__(name, "Aves")
    def speak(self):
        return f"{self.name} says: Tweet! 🐦"

zoo = [Dog("Rex", "Lab"), Cat("Luna"), Bird("Tweety")]
animal_concert(zoo)
# Rex says: Woof! 🐶
# Luna says: Meow! 🐱
# Tweety says: Tweet! 🐦

# Polymorphism with built-in operators
class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __add__(self, other):
        return Vector(self.x+other.x, self.y+other.y)
    def __str__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)  # Vector(4, 6) — + works on custom class!

Abstract Base Classes

Sometimes you want to create a parent class that cannot be instantiated directly and forces every subclass to implement certain methods. Python's ABC (Abstract Base Class) enforces this contract at instantiation time.

abstract_classes.py
PYTHON
from abc import ABC, abstractmethod

class Shape(ABC):
    """Abstract base — every shape MUST implement area and perimeter."""

    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

    # Non-abstract method — available to all subclasses
    def describe(self):
        return f"{type(self).__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}"


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    def area(self):      return 3.14159 * self.radius ** 2
    def perimeter(self): return 2 * 3.14159 * self.radius


class Rectangle(Shape):
    def __init__(self, w, h):
        self.w, self.h = w, h
    def area(self):      return self.w * self.h
    def perimeter(self): return 2 * (self.w + self.h)


# Shape()  ← TypeError: Can't instantiate abstract class

shapes = [Circle(5), Rectangle(4, 6), Circle(3)]
for s in shapes:
    print(s.describe())
# Circle: area=78.54, perimeter=31.42
# Rectangle: area=24.00, perimeter=20.00
# Circle: area=28.27, perimeter=18.85

# Total area — polymorphic sum!
total = sum(s.area() for s in shapes)
print(f"Total area: {total:.2f}")
ABC is the foundation of professional Python APIs
Python's own standard library uses ABC everywhere: collections.abc.Mapping, collections.abc.Sequence, io.IOBase. When you write a plugin system, payment gateway, or data source adapter, define an ABC contract and let each provider implement it. The rest of your code just calls the ABC's interface.

Multiple Inheritance & MRO

Python allows a class to inherit from multiple parents. When the same method exists in multiple parents, Python uses the Method Resolution Order (MRO) — determined by the C3 linearisation algorithm — to decide which one to call. Use ClassName.__mro__ to inspect it.

multiple_inheritance.py
PYTHON
class Flyable:
    def move(self): return "Flying ✈️"
    def describe(self): return "I can fly"

class Swimmable:
    def move(self): return "Swimming 🏊"
    def describe(self): return "I can swim"

class Duck(Flyable, Swimmable):  # Flyable listed first → takes priority
    def speak(self): return "Quack! 🦆"

d = Duck()
print(d.move())     # Flying ✈️  — Flyable wins (listed first)
print(d.speak())    # Quack! 🦆

# Inspect the MRO
print(Duck.__mro__)
# (, , , )

# Mixin pattern — most practical use of multiple inheritance
class JSONMixin:
    """Add JSON serialisation to any class."""
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

class ReprMixin:
    """Auto __repr__ from __dict__."""
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{type(self).__name__}({attrs})"

class Product(JSONMixin, ReprMixin):
    def __init__(self, name, price):
        self.name = name
        self.price= price

p = Product("Python Book", 1500)
print(p)             # Product(name='Python Book', price=1500)
print(p.to_json())   # {"name": "Python Book", "price": 1500}
⚠️
Prefer composition over deep inheritance chains
Inheritance deeper than 2–3 levels becomes a "fragile base class" problem — changes to a grandparent break grandchildren in unexpected ways. Use mixins (small, focused mixin classes) or composition (store an object as an attribute instead of inheriting from it) for most cases.
🧩 Knowledge Check
5 questions — Inheritance & Polymorphism
1. What does super().__init__(...) do?
2. If dog = Dog("Rex") and Dog inherits from Animal, what does isinstance(dog, Animal) return?
3. What is "duck typing" in Python?
4. What happens if you try to instantiate a class that inherits from ABC and has an unimplemented @abstractmethod?
5. In class Duck(Flyable, Swimmable), if both have a move() method, which one is called?
🏗️
Challenge — Employee Payroll System
Inheritance + polymorphism + ABC in a realistic scenario
Task: Build a payroll system with:

Abstract base Employee(ABC): attributes name, employee_id. Abstract method calculate_pay(). Concrete methods: __str__ showing name and ID, describe() showing name, type, and pay.

Three subclasses: FullTimeEmployee (monthly_salary, calculate_pay returns monthly_salary). PartTimeEmployee (hourly_rate, hours_worked, calculate_pay returns rate × hours). Contractor (daily_rate, days_worked, calculate_pay returns rate × days × 1.1 for 10% agency fee).

A run_payroll(employees) function that loops all employees, prints their describe() and returns total payroll.
💡 Show hints
  • Abstract: class Employee(ABC): and @abstractmethod def calculate_pay(self): pass
  • super().__init__(name, employee_id) in each subclass
  • describe(): f"{self.name} [{type(self).__name__}] — PKR {self.calculate_pay():,.0f}"
  • Total: return sum(e.calculate_pay() for e in employees)
payroll_system.py — Sample Solution
PYTHON
from abc import ABC, abstractmethod

class Employee(ABC):
    def __init__(self, name, employee_id):
        self.name        = name
        self.employee_id = employee_id

    @abstractmethod
    def calculate_pay(self): pass

    def describe(self):
        return f"  {self.name:18} [{type(self).__name__:20}]  PKR {self.calculate_pay():>10,.0f}"

    def __str__(self):
        return f"{self.name} (ID: {self.employee_id})"


class FullTimeEmployee(Employee):
    def __init__(self, name, eid, monthly_salary):
        super().__init__(name, eid)
        self.monthly_salary = monthly_salary
    def calculate_pay(self): return self.monthly_salary


class PartTimeEmployee(Employee):
    def __init__(self, name, eid, hourly_rate, hours):
        super().__init__(name, eid)
        self.hourly_rate  = hourly_rate
        self.hours_worked = hours
    def calculate_pay(self): return self.hourly_rate * self.hours_worked


class Contractor(Employee):
    def __init__(self, name, eid, daily_rate, days):
        super().__init__(name, eid)
        self.daily_rate   = daily_rate
        self.days_worked = days
    def calculate_pay(self): return self.daily_rate * self.days_worked * 1.1


def run_payroll(employees):
    print("═" * 60)
    print("  PAYROLL REPORT")
    print("═" * 60)
    for e in employees: print(e.describe())
    total = sum(e.calculate_pay() for e in employees)
    print("─" * 60)
    print(f"  TOTAL PAYROLL: PKR {total:,.0f}")
    return total

team = [
    FullTimeEmployee("Ali Hassan",    "FT001", 85000),
    FullTimeEmployee("Sara Khan",     "FT002", 72000),
    PartTimeEmployee("Omar Sheikh",   "PT001", 750, 80),
    Contractor("Fatima Dev",        "CT001", 5000, 15),
]
run_payroll(team)
🎉
Lesson 12 Complete!
Inheritance and polymorphism mastered! Next — reading and writing files.
← Course Home
Phase 3 · OOPLesson 12 of 6