Python Inheritance and Polymorphism: A Complete Guide to OOP Principles

Master inheritance, method overriding, abstract classes, and polymorphism for flexible code design

Python Inheritance and Polymorphism: A Complete Guide to OOP Principles



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:

Child Class / Sub Class / Derived Class:


Key Benefits of Inheritance

Code Reusability:

Extensibility:

Maintainability:



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:

Better Maintainability:

Flexibility:


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

Use abstract classes when:
  • 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:

  1. Child classes come before parent classes
  2. Parents are in the order they’re listed
  3. Each class appears only once
  4. object is 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

Choose Composition when:
  • 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
Choose Inheritance when:
  • 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:

Flexibility:

Maintainability:

Design Principles:


Remember

While inheritance is powerful, it’s not always the answer. Consider composition when:

By understanding both inheritance and composition, you can choose the right tool for each situation and build robust, maintainable applications!



References