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