Inheritance & Polymorphism
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.
# 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 — 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.
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}")
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.
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}, , )
super().__init__(...) do?dog = Dog("Rex") and Dog inherits from Animal, what does isinstance(dog, Animal) return?class Duck(Flyable, Swimmable), if both have a move() method, which one is called?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)
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)