31 min to read
Python Inheritance and Polymorphism: A Complete Guide to OOP Principles
Master inheritance, method overriding, abstract classes, and polymorphism for flexible code design
Overview
Inheritance and Polymorphism are core concepts of object-oriented programming. They enable code reusability, easier maintenance, and the design of extensible structures that can adapt to changing requirements.
Understanding inheritance and polymorphism is essential for writing clean, maintainable object-oriented code in Python. These concepts allow you to create class hierarchies that model real-world relationships, reduce code duplication, and build flexible systems that can easily accommodate new features. By mastering these principles, you’ll be able to design robust applications that are both powerful and easy to maintain.
1. Inheritance Basics
Inheritance is the mechanism by which a new class (child class) inherits attributes and methods from an existing class (parent class).
Basic Inheritance Structure
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "The animal makes a sound"
def move(self):
return f"{self.name} is moving"
class Dog(Animal): # Inherits from Animal
def speak(self): # Method overriding
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
# Usage
dog = Dog("Buddy")
cat = Cat("Whiskers")
print(dog.name) # Buddy (inherited attribute)
print(dog.speak()) # Woof! (overridden method)
print(dog.move()) # Buddy is moving (inherited method)
print(cat.speak()) # Meow!
Terminology
Parent Class / Super Class / Base Class:
- The class being inherited from (e.g., Animal)
- Provides common attributes and methods
- Defines the general behavior
Child Class / Sub Class / Derived Class:
- The class that inherits (e.g., Dog, Cat)
- Can add new attributes and methods
- Can override parent class methods
Key Benefits of Inheritance
Code Reusability:
- Write common functionality once in the parent class
- Child classes automatically get parent functionality
- Reduces code duplication
Extensibility:
- Add new classes without modifying existing code
- Extend functionality through inheritance
- Create specialized versions of general classes
Maintainability:
- Changes to parent class automatically affect all children
- Easier to fix bugs and add features
- Clear hierarchy makes code easier to understand
2. The super() Function
The super() function is used to call methods from the parent class, particularly useful for initialization and method extension.
Using super() for Initialization
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
return f"Hi, I'm {self.name}. I'm {self.age} years old."
class Student(Person):
def __init__(self, name, age, student_id):
super().__init__(name, age) # Call parent's __init__
self.student_id = student_id
def introduce(self):
parent_intro = super().introduce() # Call parent's introduce()
return f"{parent_intro} Student ID: {self.student_id}"
student = Student("John Smith", 20, "2024001")
print(student.introduce())
# Hi, I'm John Smith. I'm 20 years old. Student ID: 2024001
super() vs Direct Call
class Parent:
def method(self):
return "Parent method"
class Child(Parent):
def method(self):
# Method 1: Using super() (Recommended)
return super().method() + " + Child method"
def method_old_style(self):
# Method 2: Direct call (Not recommended)
return Parent.method(self) + " + Child method"
Why Use super()?
Proper MRO Handling:
- Follows Method Resolution Order correctly in multiple inheritance
- Ensures all parent classes are properly initialized
- Avoids calling parent methods multiple times
Better Maintainability:
- No need to hardcode parent class name
- Works correctly when parent class changes
- Makes refactoring easier
Flexibility:
- Works seamlessly with cooperative multiple inheritance
- Enables proper diamond inheritance patterns
- Supports complex class hierarchies
Advanced super() Usage
class A:
def __init__(self):
print("A init")
super().__init__()
class B:
def __init__(self):
print("B init")
super().__init__()
class C(A, B):
def __init__(self):
print("C init")
super().__init__()
c = C()
# Output:
# C init
# A init
# B init
3. Method Overriding
Method overriding allows a child class to provide a specific implementation of a method that is already defined in its parent class.
Basic Overriding Example
class Shape:
def __init__(self, color):
self.color = color
def area(self):
raise NotImplementedError("Subclasses must implement area()")
def describe(self):
return f"{self.color} shape"
class Circle(Shape):
def __init__(self, color, radius):
super().__init__(color)
self.radius = radius
def area(self): # Override
return 3.14159 * self.radius ** 2
def describe(self): # Override
return f"{super().describe()}: Circle (radius: {self.radius})"
class Rectangle(Shape):
def __init__(self, color, width, height):
super().__init__(color)
self.width = width
self.height = height
def area(self): # Override
return self.width * self.height
def describe(self):
return f"{super().describe()}: Rectangle ({self.width}x{self.height})"
circle = Circle("red", 5)
rectangle = Rectangle("blue", 4, 6)
print(circle.area()) # 78.53975
print(circle.describe()) # red shape: Circle (radius: 5)
print(rectangle.area()) # 24
print(rectangle.describe()) # blue shape: Rectangle (4x6)
Extending Parent Methods
class Logger:
def log(self, message):
print(f"[LOG] {message}")
class TimestampLogger(Logger):
def log(self, message):
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Call parent method with additional functionality
super().log(f"[{timestamp}] {message}")
logger = TimestampLogger()
logger.log("Application started")
# [LOG] [2026-05-05 10:30:45] Application started
4. Polymorphism
Polymorphism allows objects of different classes to be treated as objects of a common parent class, enabling the same interface to be used for different underlying forms.
Basic Polymorphism Example
class Bird:
def fly(self):
return "Bird flies in the sky"
class Airplane:
def fly(self):
return "Airplane flies in the sky"
class Butterfly:
def fly(self):
return "Butterfly flutters in the sky"
# Polymorphism: same method name, different behaviors
def make_it_fly(flying_object):
print(flying_object.fly())
bird = Bird()
plane = Airplane()
butterfly = Butterfly()
make_it_fly(bird) # Bird flies in the sky
make_it_fly(plane) # Airplane flies in the sky
make_it_fly(butterfly) # Butterfly flutters in the sky
Duck Typing in Python
Python follows the principle: “If it walks like a duck and quacks like a duck, it’s a duck.” Type matters less than behavior.
# No inheritance relationship needed - just need the same methods!
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
class Robot:
def speak(self):
return "Beep boop!"
def make_sound(obj):
print(obj.speak()) # Works with any type that has speak()
make_sound(Dog()) # Woof!
make_sound(Cat()) # Meow!
make_sound(Robot()) # Beep boop!
Practical Polymorphism Example
class PaymentMethod:
def process_payment(self, amount):
raise NotImplementedError
class CreditCard(PaymentMethod):
def __init__(self, card_number):
self.card_number = card_number
def process_payment(self, amount):
return f"Processing ${amount} via Credit Card ending in {self.card_number[-4:]}"
class PayPal(PaymentMethod):
def __init__(self, email):
self.email = email
def process_payment(self, amount):
return f"Processing ${amount} via PayPal ({self.email})"
class BankTransfer(PaymentMethod):
def __init__(self, account_number):
self.account_number = account_number
def process_payment(self, amount):
return f"Processing ${amount} via Bank Transfer to {self.account_number}"
# Polymorphic function
def checkout(payment_method, amount):
print(payment_method.process_payment(amount))
# All payment methods can be used interchangeably
checkout(CreditCard("1234-5678-9012-3456"), 100)
checkout(PayPal("user@example.com"), 50)
checkout(BankTransfer("987654321"), 200)
5. Abstract Base Classes (ABC)
Abstract classes define a template for other classes and cannot be instantiated directly. They enforce that child classes implement specific methods.
Creating Abstract Classes
from abc import ABC, abstractmethod
class Vehicle(ABC): # Inherit from ABC
def __init__(self, brand):
self.brand = brand
@abstractmethod
def start_engine(self):
"""Must be implemented by subclasses"""
pass
@abstractmethod
def stop_engine(self):
"""Must be implemented by subclasses"""
pass
def honk(self):
return f"{self.brand} honks: Beep beep!"
class Car(Vehicle):
def start_engine(self):
return f"{self.brand} car engine started"
def stop_engine(self):
return f"{self.brand} car engine stopped"
class Motorcycle(Vehicle):
def start_engine(self):
return f"{self.brand} motorcycle engine started"
def stop_engine(self):
return f"{self.brand} motorcycle engine stopped"
# vehicle = Vehicle("Toyota") # Error! Cannot instantiate abstract class
car = Car("Toyota")
motorcycle = Motorcycle("Honda")
print(car.start_engine()) # Toyota car engine started
print(car.honk()) # Toyota honks: Beep beep!
print(motorcycle.start_engine()) # Honda motorcycle engine started
Abstract Properties
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def sound(self):
"""Each animal must define its sound"""
pass
@property
@abstractmethod
def legs(self):
"""Each animal must define number of legs"""
pass
def describe(self):
return f"This animal says '{self.sound()}' and has {self.legs} legs"
class Dog(Animal):
def sound(self):
return "Woof"
@property
def legs(self):
return 4
class Bird(Animal):
def sound(self):
return "Tweet"
@property
def legs(self):
return 2
dog = Dog()
bird = Bird()
print(dog.describe()) # This animal says 'Woof' and has 4 legs
print(bird.describe()) # This animal says 'Tweet' and has 2 legs
When to Use Abstract Classes
- You want to define an interface/contract for subclasses
- You need to ensure certain methods are implemented
- You want to share common code among related classes
- You're building a framework or library
6. Multiple Inheritance
Python supports inheriting from multiple parent classes, though it should be used carefully.
Basic Multiple Inheritance
class Flyable:
def fly(self):
return "Can fly"
class Swimmable:
def swim(self):
return "Can swim"
class Duck(Flyable, Swimmable):
def __init__(self, name):
self.name = name
def quack(self):
return "Quack!"
duck = Duck("Donald")
print(duck.fly()) # Can fly
print(duck.swim()) # Can swim
print(duck.quack()) # Quack!
Method Resolution Order (MRO)
MRO determines the order in which parent classes are searched when looking for a method.
class A:
def method(self):
return "A"
class B(A):
def method(self):
return "B"
class C(A):
def method(self):
return "C"
class D(B, C):
pass
d = D()
print(d.method()) # B
print(D.mro()) # View MRO
# [<class '__main__.D'>, <class '__main__.B'>,
# <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
MRO Rules
The C3 Linearization Algorithm:
- Child classes come before parent classes
- Parents are in the order they’re listed
- Each class appears only once
objectis always at the end
The Diamond Problem
class Base:
def __init__(self):
print("Base init")
super().__init__()
class Left(Base):
def __init__(self):
print("Left init")
super().__init__()
class Right(Base):
def __init__(self):
print("Right init")
super().__init__()
class Child(Left, Right):
def __init__(self):
print("Child init")
super().__init__()
child = Child()
# Output:
# Child init
# Left init
# Right init
# Base init
print(Child.mro())
# [Child, Left, Right, Base, object]
Mixin Pattern
A common use of multiple inheritance is the mixin pattern for adding functionality.
class JSONMixin:
def to_json(self):
import json
return json.dumps(self.__dict__)
class XMLMixin:
def to_xml(self):
xml = "<object>"
for key, value in self.__dict__.items():
xml += f"<{key}>{value}</{key}>"
xml += "</object>"
return xml
class Person(JSONMixin, XMLMixin):
def __init__(self, name, age):
self.name = name
self.age = age
person = Person("Alice", 30)
print(person.to_json()) # {"name": "Alice", "age": 30}
print(person.to_xml()) # <object><name>Alice</name><age>30</age></object>
7. Practical Example: Employee Management System
A comprehensive example utilizing all concepts covered so far.
from abc import ABC, abstractmethod
from datetime import datetime
class Employee(ABC):
"""Abstract Employee class"""
employee_count = 0
def __init__(self, name, employee_id):
self.name = name
self.employee_id = employee_id
self.hire_date = datetime.now()
Employee.employee_count += 1
@abstractmethod
def calculate_salary(self):
"""Calculate salary - must be implemented by subclasses"""
pass
@abstractmethod
def get_role(self):
"""Return job role - must be implemented by subclasses"""
pass
def get_info(self):
return f"[{self.employee_id}] {self.name} - {self.get_role()}"
@classmethod
def get_employee_count(cls):
return cls.employee_count
class Developer(Employee):
"""Developer class"""
def __init__(self, name, employee_id, programming_languages, level):
super().__init__(name, employee_id)
self.programming_languages = programming_languages
self.level = level # junior, senior, lead
def calculate_salary(self):
base_salary = 60000
level_bonus = {
'junior': 0,
'senior': 20000,
'lead': 40000
}
return base_salary + level_bonus.get(self.level, 0)
def get_role(self):
return f"{self.level.capitalize()} Developer"
def write_code(self, language):
if language in self.programming_languages:
return f"{self.name} is writing code in {language}"
return f"{self.name} doesn't know {language}"
class Designer(Employee):
"""Designer class"""
def __init__(self, name, employee_id, specialty):
super().__init__(name, employee_id)
self.specialty = specialty # UI/UX, Graphic, etc.
def calculate_salary(self):
return 70000
def get_role(self):
return f"{self.specialty} Designer"
def create_design(self, project):
return f"{self.name} is creating design for {project}"
class Manager(Employee):
"""Manager class"""
def __init__(self, name, employee_id, team_size):
super().__init__(name, employee_id)
self.team_size = team_size
self.team_members = []
def calculate_salary(self):
base_salary = 100000
team_bonus = self.team_size * 2000
return base_salary + team_bonus
def get_role(self):
return f"Manager ({self.team_size} team members)"
def add_team_member(self, employee):
self.team_members.append(employee)
self.team_size = len(self.team_members)
def get_team_info(self):
team_info = [f"\n{self.name}'s Team:"]
for member in self.team_members:
team_info.append(f" - {member.get_info()}")
return "\n".join(team_info)
# Polymorphism in action
def print_salary_info(employees):
print("=== Salary Report ===")
total = 0
for emp in employees:
salary = emp.calculate_salary()
total += salary
print(f"{emp.get_info()}: ${salary:,}")
print(f"\nTotal Payroll: ${total:,}")
print(f"Total Employees: {Employee.get_employee_count()}")
# Usage example
dev1 = Developer("Alice Johnson", "DEV001", ["Python", "JavaScript"], "senior")
dev2 = Developer("Bob Smith", "DEV002", ["Java", "Kotlin"], "junior")
designer = Designer("Carol White", "DES001", "UI/UX")
manager = Manager("David Brown", "MGR001", 0)
# Build team
manager.add_team_member(dev1)
manager.add_team_member(dev2)
manager.add_team_member(designer)
# Employee list
employees = [dev1, dev2, designer, manager]
# Polymorphism: same function works with all employee types
print_salary_info(employees)
print(manager.get_team_info())
print(f"\n{dev1.write_code('Python')}")
print(designer.create_design("E-commerce App"))
Output
=== Salary Report ===
[DEV001] Alice Johnson - Senior Developer: $80,000
[DEV002] Bob Smith - Junior Developer: $60,000
[DES001] Carol White - UI/UX Designer: $70,000
[MGR001] David Brown - Manager (3 team members): $106,000
Total Payroll: $316,000
Total Employees: 4
David Brown's Team:
- [DEV001] Alice Johnson - Senior Developer
- [DEV002] Bob Smith - Junior Developer
- [DES001] Carol White - UI/UX Designer
Alice Johnson is writing code in Python
Carol White is creating design for E-commerce App
8. Inheritance vs Composition
Not every relationship should use inheritance. Understanding when to use inheritance versus composition is crucial for good design.
The IS-A vs HAS-A Principle
# Inheritance (IS-A): A car IS A vehicle
class Vehicle:
pass
class Car(Vehicle): # Car IS-A Vehicle
pass
# Composition (HAS-A): A car HAS AN engine
class Engine:
def start(self):
return "Engine starting"
class Car:
def __init__(self):
self.engine = Engine() # Car HAS-AN Engine
def start(self):
return self.engine.start()
When to Use Composition
# Bad: Overly deep inheritance hierarchy
class Vehicle:
pass
class MotorVehicle(Vehicle):
pass
class Car(MotorVehicle):
pass
class SportsCar(Car):
pass
# Better: Use composition for flexibility
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
return f"Engine ({self.horsepower}hp) starting"
class Transmission:
def __init__(self, type):
self.type = type
def shift(self):
return f"Shifting {self.type} transmission"
class Car:
def __init__(self, engine, transmission):
self.engine = engine
self.transmission = transmission
def drive(self):
return f"{self.engine.start()}. {self.transmission.shift()}"
# Easy to create different combinations
sports_car = Car(Engine(400), Transmission("manual"))
family_car = Car(Engine(180), Transmission("automatic"))
print(sports_car.drive())
# Engine (400hp) starting. Shifting manual transmission
print(family_car.drive())
# Engine (180hp) starting. Shifting automatic transmission
Favor Composition Over Inheritance
- The inheritance hierarchy becomes too deep
- You need to combine multiple features flexibly
- The HAS-A relationship is more natural than IS-A
- You want to avoid tight coupling
- There's a clear IS-A relationship
- You want to share interface and implementation
- Subclasses are true specializations of the parent
- You're defining a type hierarchy
Practical Example: Logger System
# Using Composition for flexibility
class FileWriter:
def write(self, message):
with open('log.txt', 'a') as f:
f.write(message + '\n')
class ConsoleWriter:
def write(self, message):
print(message)
class DatabaseWriter:
def write(self, message):
# Simulate database write
print(f"DB: {message}")
class Logger:
def __init__(self, writers):
self.writers = writers # Composition: Logger HAS writers
def log(self, message):
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
formatted = f"[{timestamp}] {message}"
for writer in self.writers:
writer.write(formatted)
# Flexible configuration
file_logger = Logger([FileWriter()])
console_logger = Logger([ConsoleWriter()])
multi_logger = Logger([FileWriter(), ConsoleWriter(), DatabaseWriter()])
multi_logger.log("Application started")
# Writes to file, console, and database simultaneously
Key Concepts Summary
| Concept | Description | When to Use |
|---|---|---|
| Inheritance | Child inherits from parent | IS-A relationship, code reuse |
| Overriding | Redefine parent method | Change behavior in subclass |
| super() | Call parent method | Extend parent functionality |
| Polymorphism | Same interface, different behavior | Flexible code design |
| Abstract Class | Template enforcing implementation | Define interface |
| Multiple Inheritance | Inherit from multiple parents | Combine multiple features |
| Composition | Contain objects | HAS-A relationship |
Best Practices
Practice 1: Keep Inheritance Hierarchies Shallow
Bad:
class A:
pass
class B(A):
pass
class C(B):
pass
class D(C):
pass
class E(D): # Too deep!
pass
Good:
class Base:
pass
class TypeA(Base):
pass
class TypeB(Base):
pass
Practice 2: Use Abstract Classes for Interfaces
from abc import ABC, abstractmethod
class DataProcessor(ABC):
@abstractmethod
def process(self, data):
"""All processors must implement this"""
pass
class JSONProcessor(DataProcessor):
def process(self, data):
import json
return json.loads(data)
class XMLProcessor(DataProcessor):
def process(self, data):
# XML processing logic
pass
Practice 3: Avoid Multiple Inheritance for State
# Bad: Multiple inheritance with state
class A:
def __init__(self):
self.value = 1
class B:
def __init__(self):
self.value = 2
class C(A, B): # Which value?
pass
# Good: Use composition or single inheritance
class Container:
def __init__(self, component_a, component_b):
self.a = component_a
self.b = component_b
Conclusion
Mastering inheritance and polymorphism is essential for effective object-oriented programming in Python.
Key Takeaways
Code Reusability:
- Inheritance eliminates code duplication
- Share common functionality across related classes
- Build upon existing implementations
Flexibility:
- Polymorphism enables flexible design
- Write code that works with multiple types
- Easy to extend with new classes
Maintainability:
- Clear class hierarchies improve understanding
- Changes propagate through inheritance
- Abstract classes enforce consistent interfaces
Design Principles:
- Favor composition over inheritance when appropriate
- Keep hierarchies shallow and focused
- Use abstract classes to define clear contracts
- Apply polymorphism for flexible, extensible code
Remember
While inheritance is powerful, it’s not always the answer. Consider composition when:
- Inheritance hierarchy becomes too complex
- You need flexible feature combinations
- HAS-A relationship is more natural than IS-A
- You want to avoid tight coupling
By understanding both inheritance and composition, you can choose the right tool for each situation and build robust, maintainable applications!
References
- Python Official Documentation - ABC Module
- Python Official Documentation - Method Resolution Order
- GeeksforGeeks - Method Resolution Order in Python
- DataCamp - Python Inheritance Tutorial
- AlmaBetter - Python Inheritance and Polymorphism
- DataFlair - Python Multiple Inheritance
- CoderzColumn - Method Resolution Order in Multiple Inheritance
Comments