Skip to main content

05.2 - Inheritance & Polymorphism

Theory 30 min Intermediate

Inheritance

Inheritance lets a class (child/subclass) inherit attributes and methods from another class (parent/superclass).

class Animal:
"""Base class for all animals."""

def __init__(self, name: str, sound: str):
self.name = name
self.sound = sound

def speak(self) -> str:
return f"{self.name} says: {self.sound}!"

def __str__(self) -> str:
return f"{self.__class__.__name__}({self.name})"


class Dog(Animal):
"""A dog that can fetch."""

def __init__(self, name: str, breed: str):
super().__init__(name, "Woof") # call parent __init__
self.breed = breed

def fetch(self, item: str) -> str:
return f"{self.name} fetches the {item}!"


class Cat(Animal):
def __init__(self, name: str, indoor: bool = True):
super().__init__(name, "Meow")
self.indoor = indoor

def purr(self) -> str:
return f"{self.name} purrs... 😸"


rex = Dog("Rex", "German Shepherd")
whiskers = Cat("Whiskers")

rex.speak() # Rex says: Woof!
rex.fetch("ball") # Rex fetches the ball!
whiskers.speak() # Whiskers says: Meow!

super() — Calling Parent Methods

super() calls the parent class method — essential in __init__ to initialize parent state:

class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year

def info(self):
return f"{self.year} {self.make} {self.model}"


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

def info(self):
base = super().info() # reuse parent method
return f"{base} ({self.doors} doors)"


class ElectricCar(Car):
def __init__(self, make, model, year, battery_kwh):
super().__init__(make, model, year)
self.battery_kwh = battery_kwh

def info(self):
base = super().info()
return f"{base}{self.battery_kwh}kWh battery"


tesla = ElectricCar("Tesla", "Model 3", 2024, 82)
tesla.info() # 2024 Tesla Model 3 (4 doors) — 82kWh battery

Method Resolution Order (MRO)

Python uses the C3 linearization algorithm to determine method lookup order in multiple inheritance:

class A:
def hello(self): return "A"

class B(A):
def hello(self): return "B"

class C(A):
def hello(self): return "C"

class D(B, C):
pass

D.__mro__
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

D().hello() # "B" — follows MRO left-to-right

Polymorphism

Polymorphism means different classes respond to the same method call:

class Shape:
def area(self) -> float:
raise NotImplementedError

def describe(self):
return f"{self.__class__.__name__}: area = {self.area():.2f}"


class Circle(Shape):
def __init__(self, radius):
self.radius = radius

def area(self):
import math
return math.pi * self.radius ** 2


class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height

def area(self):
return self.width * self.height


class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height

def area(self):
return 0.5 * self.base * self.height


# Polymorphic behavior — same interface, different implementations
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]

for shape in shapes:
print(shape.describe()) # each calls its own area()
# Circle: area = 78.54
# Rectangle: area = 24.00
# Triangle: area = 12.00

Abstract Classes

Abstract classes define an interface that subclasses must implement:

from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
"""Abstract base for all payment processors."""

@abstractmethod
def charge(self, amount: float) -> bool:
"""Charge the given amount. Must return True on success."""
...

@abstractmethod
def refund(self, amount: float) -> bool:
"""Issue a refund."""
...

def process(self, amount: float) -> str:
"""Template method — shared logic."""
if self.charge(amount):
return f"Payment of ${amount:.2f} processed"
return "Payment failed"


class StripeProcessor(PaymentProcessor):
def charge(self, amount):
print(f"Stripe: charging ${amount:.2f}")
return True

def refund(self, amount):
print(f"Stripe: refunding ${amount:.2f}")
return True


class PayPalProcessor(PaymentProcessor):
def charge(self, amount):
print(f"PayPal: charging ${amount:.2f}")
return True

def refund(self, amount):
print(f"PayPal: refunding ${amount:.2f}")
return True


# PaymentProcessor() # TypeError: Can't instantiate abstract class!
stripe = StripeProcessor()
stripe.process(99.99) # Stripe: charging $99.99 / Payment of $99.99 processed

isinstance() and issubclass()

tesla = ElectricCar("Tesla", "Model 3", 2024, 82)

isinstance(tesla, ElectricCar) # True
isinstance(tesla, Car) # True (is a Car)
isinstance(tesla, Vehicle) # True (is a Vehicle)
isinstance(tesla, Animal) # False

issubclass(ElectricCar, Car) # True
issubclass(ElectricCar, Vehicle) # True
issubclass(Dog, Vehicle) # False

Mixin Pattern

Mixins add specific capabilities without full inheritance:

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


class SerializeMixin:
def to_dict(self):
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}


class User(LogMixin, SerializeMixin):
def __init__(self, name, email):
self.name = name
self.email = email

def activate(self):
self.log(f"Activating user {self.name}")


user = User("Alice", "alice@example.com")
user.activate() # [User] Activating user Alice
user.to_dict() # {'name': 'Alice', 'email': 'alice@example.com'}

Key Vocabulary

TermDefinition
InheritanceChild class acquires attributes/methods from parent class
super()Calls the parent class method
Method overridingRedefining a parent method in a child class
PolymorphismDifferent classes respond to the same method call
Abstract classClass with abstract methods that must be implemented by subclasses
ABCAbstract Base Class from abc module
@abstractmethodForces subclasses to implement a method
MROMethod Resolution Order — determines method lookup in inheritance
MixinA class providing optional functionality via multiple inheritance

Summary

  • Inheritance: class Child(Parent) — child gets all parent methods and attributes
  • Always call super().__init__(...) in child __init__ to initialize parent state
  • Method overriding: redefine a parent method in the child class
  • Polymorphism: write code against an interface, let objects decide behavior
  • Abstract classes (ABC + @abstractmethod) enforce contracts on subclasses
  • The MRO determines method lookup order in multiple inheritance (C3 algorithm)