Polymorphism and Abstraction in Python

Introduction

Polymorphism and abstraction are advanced OOP concepts that make code more flexible, maintainable, and easier to understand.

Note

Polymorphism means “many forms” - the ability to use the same interface for different data types. Abstraction means hiding complex implementation details and showing only essential features.


What is Polymorphism?

Polymorphism allows objects of different classes to be treated as objects of a common parent class. The same method name can behave differently based on the object that calls it.

Types of Polymorphism:

  1. Method Overriding (Runtime Polymorphism)

  2. Operator Overloading (Compile-time Polymorphism)

  3. Duck Typing (Python-specific)


Method Overriding - Runtime Polymorphism

Child classes can override parent class methods to provide specific implementations.

class Animal:
    def speak(self):
        return "Animal makes a sound"

class Dog(Animal):
    def speak(self):
        return "Dog barks: Woof Woof!"

class Cat(Animal):
    def speak(self):
        return "Cat meows: Meow Meow!"

class Cow(Animal):
    def speak(self):
        return "Cow moos: Moo Moo!"

# Polymorphism in action
animals = [Dog(), Cat(), Cow()]

for animal in animals:
    print(animal.speak())  # Same method, different behavior

# Output:
# Dog barks: Woof Woof!
# Cat meows: Meow Meow!
# Cow moos: Moo Moo!

Note

Polymorphism allows you to write more generic code that works with objects of different types through a common interface.


Real-World Example: Payment System

class Payment:
    def __init__(self, amount):
        self.amount = amount

    def process_payment(self):
        return "Processing generic payment"

class CreditCardPayment(Payment):
    def __init__(self, amount, card_number):
        super().__init__(amount)
        self.card_number = card_number

    def process_payment(self):
        return f"Processing credit card payment of ₹{self.amount} using card {self.card_number[-4:]}"

class UPIPayment(Payment):
    def __init__(self, amount, upi_id):
        super().__init__(amount)
        self.upi_id = upi_id

    def process_payment(self):
        return f"Processing UPI payment of ₹{self.amount} to {self.upi_id}"

class CashPayment(Payment):
    def process_payment(self):
        return f"Processing cash payment of ₹{self.amount}"

# Polymorphic behavior
def make_payment(payment_obj):
    print(payment_obj.process_payment())

# Same function, different behaviors
make_payment(CreditCardPayment(1000, "1234-5678-9012-3456"))
make_payment(UPIPayment(500, "user@paytm"))
make_payment(CashPayment(200))

Operator Overloading

Python allows you to define how operators work with custom objects using special methods (magic methods).

Common Magic Methods:

  • __add__(self, other) for +

  • __sub__(self, other) for -

  • __mul__(self, other) for *

  • __eq__(self, other) for ==

  • __lt__(self, other) for <

  • __str__(self) for str()

  • __len__(self) for len()

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __str__(self):
        return f"Vector({self.x}, {self.y})"

# Using overloaded operators
v1 = Vector(3, 4)
v2 = Vector(1, 2)

v3 = v1 + v2  # Calls __add__
print(v3)     # Vector(4, 6)

v4 = v1 - v2  # Calls __sub__
print(v4)     # Vector(2, 2)

v5 = v1 * 2   # Calls __mul__
print(v5)     # Vector(6, 8)

print(v1 == v2)  # False (calls __eq__)

Practical Operator Overloading: Book Comparison

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages

    def __str__(self):
        return f"'{self.title}' by {self.author} ({self.pages} pages)"

    def __eq__(self, other):
        return self.pages == other.pages

    def __lt__(self, other):
        return self.pages < other.pages

    def __gt__(self, other):
        return self.pages > other.pages

    def __add__(self, other):
        # Combine two books (total pages)
        return self.pages + other.pages

book1 = Book("Python Basics", "John", 250)
book2 = Book("Advanced Python", "Jane", 450)
book3 = Book("Python Projects", "Bob", 250)

print(book1)                    # 'Python Basics' by John (250 pages)
print(book1 == book3)           # True (same pages)
print(book1 < book2)            # True (250 < 450)
print(f"Total pages: {book1 + book2}")  # Total pages: 700

Duck Typing in Python

“If it walks like a duck and quacks like a duck, it must be a duck.” Python uses duck typing - it doesn’t check the type, but whether the object has the required methods.

class Dog:
    def speak(self):
        return "Woof!"

class Cat:
    def speak(self):
        return "Meow!"

class Radio:
    def speak(self):
        return "Music playing..."

# Function doesn't care about type, only that speak() exists
def make_it_speak(obj):
    print(obj.speak())

# Duck typing - all work because they have speak()
make_it_speak(Dog())    # Woof!
make_it_speak(Cat())    # Meow!
make_it_speak(Radio())  # Music playing...

Note

Duck typing makes Python flexible - you don’t need inheritance for polymorphism, just consistent interfaces.


What is Abstraction?

Abstraction means hiding complex implementation details and exposing only necessary functionality. It helps in:

  • Reducing complexity

  • Defining clear interfaces

  • Enforcing method implementation in child classes

Python provides the abc (Abstract Base Class) module for creating abstract classes.


Abstract Base Classes (ABC)

An abstract class cannot be instantiated and may contain abstract methods that must be implemented by child classes.

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

    def description(self):  # Concrete method
        return "This is a shape"

# Cannot create object of abstract class
# shape = Shape()  # TypeError: Can't instantiate abstract class

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

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

    def perimeter(self):
        return 2 * (self.length + self.width)

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

# Now we can create objects of concrete classes
rect = Rectangle(10, 5)
print(f"Rectangle Area: {rect.area()}")         # 50
print(f"Rectangle Perimeter: {rect.perimeter()}")  # 30

circle = Circle(7)
print(f"Circle Area: {circle.area():.2f}")      # 153.94
print(f"Circle Perimeter: {circle.perimeter():.2f}")  # 43.98

Note

Abstract methods must be implemented by all child classes. This ensures a consistent interface across different implementations.


Real-World Example: Database Connections

from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def connect(self):
        pass

    @abstractmethod
    def disconnect(self):
        pass

    @abstractmethod
    def execute_query(self, query):
        pass

class MySQLDatabase(Database):
    def connect(self):
        return "Connected to MySQL database"

    def disconnect(self):
        return "Disconnected from MySQL database"

    def execute_query(self, query):
        return f"Executing MySQL query: {query}"

class PostgreSQLDatabase(Database):
    def connect(self):
        return "Connected to PostgreSQL database"

    def disconnect(self):
        return "Disconnected from PostgreSQL database"

    def execute_query(self, query):
        return f"Executing PostgreSQL query: {query}"

class MongoDBDatabase(Database):
    def connect(self):
        return "Connected to MongoDB database"

    def disconnect(self):
        return "Disconnected from MongoDB database"

    def execute_query(self, query):
        return f"Executing MongoDB query: {query}"

# Polymorphic function
def perform_database_operations(db):
    print(db.connect())
    print(db.execute_query("SELECT * FROM users"))
    print(db.disconnect())
    print()

# Works with any database type
perform_database_operations(MySQLDatabase())
perform_database_operations(PostgreSQLDatabase())
perform_database_operations(MongoDBDatabase())

Complete Example: Employee Management System

from abc import ABC, abstractmethod

class Employee(ABC):
    def __init__(self, name, emp_id):
        self.name = name
        self.emp_id = emp_id

    @abstractmethod
    def calculate_salary(self):
        pass

    @abstractmethod
    def get_role(self):
        pass

    def display_info(self):
        return f"Name: {self.name}, ID: {self.emp_id}, Role: {self.get_role()}"

class FullTimeEmployee(Employee):
    def __init__(self, name, emp_id, monthly_salary):
        super().__init__(name, emp_id)
        self.monthly_salary = monthly_salary

    def calculate_salary(self):
        return self.monthly_salary

    def get_role(self):
        return "Full-Time Employee"

class ContractEmployee(Employee):
    def __init__(self, name, emp_id, hourly_rate, hours_worked):
        super().__init__(name, emp_id)
        self.hourly_rate = hourly_rate
        self.hours_worked = hours_worked

    def calculate_salary(self):
        return self.hourly_rate * self.hours_worked

    def get_role(self):
        return "Contract Employee"

class Intern(Employee):
    def __init__(self, name, emp_id, stipend):
        super().__init__(name, emp_id)
        self.stipend = stipend

    def calculate_salary(self):
        return self.stipend

    def get_role(self):
        return "Intern"

# Payroll function using polymorphism
def process_payroll(employees):
    total = 0
    for emp in employees:
        salary = emp.calculate_salary()
        print(f"{emp.display_info()} - Salary: ₹{salary}")
        total += salary
    print(f"\nTotal Payroll: ₹{total}")

# Create different types of employees
employees = [
    FullTimeEmployee("Alice", "FT001", 50000),
    ContractEmployee("Bob", "CT001", 500, 160),
    Intern("Charlie", "IN001", 15000)
]

process_payroll(employees)

Abstract Properties

You can also create abstract properties that must be implemented in child classes.

from abc import ABC, abstractmethod

class Vehicle(ABC):
    @abstractmethod
    def start(self):
        pass

    @property
    @abstractmethod
    def max_speed(self):
        pass

class Car(Vehicle):
    def __init__(self, brand):
        self.brand = brand
        self._max_speed = 180

    def start(self):
        return f"{self.brand} car engine started"

    @property
    def max_speed(self):
        return self._max_speed

car = Car("Tesla")
print(car.start())       # Tesla car engine started
print(car.max_speed)     # 180

Tasks

Task 1: Media Player Polymorphism

Create an abstract class MediaPlayer with abstract methods play(), pause(), and stop(). Create child classes MusicPlayer and VideoPlayer with specific implementations.

Hint: Use from abc import ABC, abstractmethod. Each child class must implement all abstract methods.

Task 2: Operator Overloading for Coordinates

Create a Point class representing 2D coordinates. Overload + (add points), - (subtract points), == (compare points), and __str__ (display format).

Hint: Use __add__, __sub__, __eq__, and __str__ methods. For addition: new point = (x1+x2, y1+y2).

Task 3: Abstract Shape Calculator

Create abstract class Shape with abstract methods area() and perimeter(). Implement Triangle, Square, and Circle classes. Create a function to calculate total area of mixed shapes.

Hint: Store shapes in a list and iterate to calculate total. Triangle area = 0.5 × base × height.

Task 4: Banking Operations with Abstraction

Create abstract class Transaction with abstract method execute(). Implement Deposit, Withdrawal, and Transfer classes. Each should execute differently.

Hint: Pass account balance to execute() method. Return updated balance after operation.

Task 5: Notification System

Create abstract class Notification with abstract method send(). Implement EmailNotification, SMSNotification, and PushNotification classes. Create a function to send notifications to a list of users.

Hint: Each notification type should have different send() implementation showing how the message is delivered.


Summary

Polymorphism: - Allows same interface for different implementations - Method overriding enables runtime polymorphism - Operator overloading customizes operator behavior - Duck typing provides flexible polymorphism without inheritance

Abstraction: - Hides complexity and shows only essential features - Abstract classes define contracts for child classes - Use ABC and @abstractmethod from abc module - Abstract classes cannot be instantiated directly - Child classes must implement all abstract methods - Provides clear interfaces and enforces consistency