Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
20 minLesson 12 of 34
Object-Oriented Python

Inheritance & Polymorphism

Inheritance & Polymorphism: Building on What Exists

Inheritance lets you create new classes based on existing ones, inheriting their attributes and methods. Polymorphism lets different objects respond to the same interface differently. Together, they're what makes OOP powerful — you write once and extend many times.

Basic Inheritance

# Base class (parent / superclass)
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.is_alive = True
    
    def eat(self, food):
        print(f"{self.name} eats {food}")
    
    def sleep(self):
        print(f"{self.name} sleeps")
    
    def describe(self):
        return f"{self.name}, age {self.age}"
    
    def speak(self):
        raise NotImplementedError("Subclasses must implement speak()")

# Child class (subclass) inherits from Animal
class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)  # Call parent __init__
        self.breed = breed
    
    def speak(self):
        return f"{self.name} says: Woof!"
    
    def fetch(self, item):
        return f"{self.name} fetches the {item}!"
    
    def describe(self):
        base = super().describe()      # Call parent method
        return f"{base} ({self.breed})"

class Cat(Animal):
    def __init__(self, name, age, indoor=True):
        super().__init__(name, age)
        self.indoor = indoor
    
    def speak(self):
        return f"{self.name} says: Meow!"
    
    def purr(self):
        return f"{self.name} purrs..."

# Usage
dog = Dog("Rex", 3, "German Shepherd")
cat = Cat("Whiskers", 5)

print(dog.speak())         # Rex says: Woof!
print(cat.speak())         # Whiskers says: Meow!
print(dog.describe())      # Rex, age 3 (German Shepherd)
dog.eat("bones")           # Rex eats bones (inherited from Animal)
print(dog.fetch("ball"))   # Rex fetches the ball!

# isinstance checks inheritance chain
print(isinstance(dog, Dog))     # True
print(isinstance(dog, Animal))  # True — Dog IS-A Animal
print(isinstance(dog, Cat))     # False

# issubclass
print(issubclass(Dog, Animal))  # True
print(issubclass(Cat, Animal))  # True

Polymorphism: Same Interface, Different Behavior

Polymorphism means you can treat different objects the same way through a shared interface.

animals = [
    Dog("Rex", 3, "Lab"),
    Cat("Luna", 2),
    Dog("Max", 5, "Poodle"),
    Cat("Oliver", 7, indoor=False)
]

# Each speaks differently, but we call the same method on all
for animal in animals:
    print(animal.speak())

# Function that works with any Animal
def make_noise(animal: Animal):
    """Works with any subclass of Animal — polymorphism in action."""
    return animal.speak()

# This works even with future Animal subclasses we haven't written yet

Abstract Classes: Enforcing Contracts

Use ABC to declare that subclasses MUST implement certain methods.

from abc import ABC, abstractmethod

class Shape(ABC):
    def __init__(self, color="black"):
        self.color = color
    
    @abstractmethod
    def area(self) -> float:
        """Subclasses must implement this."""
        pass
    
    @abstractmethod
    def perimeter(self) -> float:
        pass
    
    def describe(self):
        # Concrete method — shared by all shapes
        return (f"{self.color} {self.__class__.__name__}: "
                f"area={self.area():.2f}, perimeter={self.perimeter():.2f}")

class Circle(Shape):
    def __init__(self, radius, color="black"):
        super().__init__(color)
        self.radius = radius
    
    def area(self):
        import math
        return math.pi * self.radius ** 2
    
    def perimeter(self):
        import math
        return 2 * math.pi * self.radius

class Rectangle(Shape):
    def __init__(self, width, height, color="black"):
        super().__init__(color)
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def perimeter(self):
        return 2 * (self.width + self.height)

class Triangle(Shape):
    def __init__(self, a, b, c, color="black"):
        super().__init__(color)
        self.a, self.b, self.c = a, b, c
    
    def area(self):
        s = self.perimeter() / 2  # Semi-perimeter
        return (s * (s-self.a) * (s-self.b) * (s-self.c)) ** 0.5
    
    def perimeter(self):
        return self.a + self.b + self.c

# Can't instantiate abstract class
try:
    s = Shape()  # TypeError: Can't instantiate abstract class
except TypeError as e:
    print(e)

# Works with concrete subclasses
shapes = [Circle(5, "red"), Rectangle(4, 6, "blue"), Triangle(3, 4, 5)]
for shape in shapes:
    print(shape.describe())

# Total area — works with any Shape
total = sum(s.area() for s in shapes)
print(f"Total area: {total:.2f}")

Multiple Inheritance and MRO

Python supports multiple inheritance — a class can inherit from multiple parents.

class Flyable:
    def fly(self):
        return f"{self.name} flies through the air!"
    
    def altitude(self):
        return "Up in the sky"

class Swimmable:
    def swim(self):
        return f"{self.name} swims in the water!"
    
    def depth(self):
        return "Under the surface"

class Duck(Animal, Flyable, Swimmable):
    def speak(self):
        return f"{self.name} says: Quack!"

duck = Duck("Donald", 3)
print(duck.speak())    # Donald says: Quack!
print(duck.fly())      # Donald flies through the air!
print(duck.swim())     # Donald swims in the water!

# Method Resolution Order (MRO) — Python's rules for which method to use
print(Duck.__mro__)
# (<class 'Duck'>, <class 'Animal'>, <class 'Flyable'>, <class 'Swimmable'>, <class 'object'>)

Composition vs Inheritance: The Important Choice

Inheritance models "is-a" relationships. Composition models "has-a" relationships. Experienced developers often prefer composition.

# Inheritance approach: Car IS-A Vehicle
class Vehicle:
    def __init__(self, make, model):
        self.make = make
        self.model = model
    
    def start(self):
        print("Engine started")

class Car(Vehicle):
    def __init__(self, make, model, doors):
        super().__init__(make, model)
        self.doors = doors

# Composition approach: Car HAS-A Engine
class Engine:
    def __init__(self, horsepower, fuel_type):
        self.horsepower = horsepower
        self.fuel_type = fuel_type
    
    def start(self):
        return "Engine started"
    
    def describe(self):
        return f"{self.horsepower}hp {self.fuel_type} engine"

class GPS:
    def navigate(self, destination):
        return f"Navigating to {destination}"

class Car:
    def __init__(self, make, model, engine, has_gps=False):
        self.make = make
        self.model = model
        self.engine = engine  # HAS-A engine
        self.gps = GPS() if has_gps else None
    
    def start(self):
        return self.engine.start()  # Delegate to engine
    
    def navigate(self, destination):
        if self.gps is None:
            raise ValueError("No GPS installed")
        return self.gps.navigate(destination)

tesla = Car("Tesla", "Model S", Engine(670, "electric"), has_gps=True)
print(tesla.start())
print(tesla.navigate("San Francisco"))
print(tesla.engine.describe())

When to use inheritance: The child truly IS-A parent (Dog IS-A Animal; Circle IS-A Shape)

When to use composition: The child HAS-A component (Car HAS-A Engine; User HAS-A Address)

Mixin Classes: Reusable Behavior

Mixins add specific functionality to classes without deep inheritance hierarchies.

class JSONMixin:
    """Adds JSON serialization to any class."""
    import json
    
    def to_json(self):
        import json
        return json.dumps(self.__dict__, default=str)
    
    @classmethod
    def from_json(cls, json_string):
        import json
        data = json.loads(json_string)
        return cls(**data)

class LogMixin:
    """Adds logging to any class."""
    def log(self, message):
        print(f"[{self.__class__.__name__}] {message}")

class Product(JSONMixin, LogMixin):
    def __init__(self, name, price, category):
        self.name = name
        self.price = price
        self.category = category
    
    def apply_discount(self, pct):
        old_price = self.price
        self.price = round(self.price * (1 - pct/100), 2)
        self.log(f"Discount applied: ${old_price} → ${self.price}")

laptop = Product("MacBook", 2499.99, "Electronics")
laptop.apply_discount(10)

json_str = laptop.to_json()
print(json_str)

restored = Product.from_json(json_str)
print(restored.name, restored.price)

Mixins are a practical alternative to deeply nested inheritance trees — they keep responsibilities separated and classes composable.

Next lesson: Magic Methods & Operator Overloading — making your classes behave like built-ins.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!