← Computer Programming II

Polymorphism Spider-Man meme

What Is Polymorphism?

The term polymorphism is derived from Greek: poly means many, and morph means form. In object-oriented programming, polymorphism refers to the idea of one interface, many implementations. A program may call the same method or operation on different objects, and each object may respond according to its own implementation.


Polymorphism Through Inheritance

A classical form of polymorphism appears through inheritance and method overriding. Consider a payroll system:

class Employee:
    def __init__(self, name):
        self.name = name
    def calculate_pay(self):
        return 0

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

    def calculate_pay(self):
        return self.monthly_salary

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

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

class Intern(Employee):
    def calculate_pay(self):
        return 0  # sorry, interns

Each subclass provides its own version of calculate_pay(). This allows the same method call to produce different results depending on the employee type.

employees = [
    FullTimeEmployee("Alisher", 5_000_000),
    PartTimeEmployee("Sevara", 80, 50_000),
    Intern("Jasur"),
]

total_payroll = 0
for emp in employees:
    pay = emp.calculate_pay()
    print(f"{emp.name}: {pay:,} sum")
    total_payroll += pay

print(f"Total: {total_payroll:,} sum")
Alisher: 5,000,000 sum
Sevara: 4,000,000 sum
Jasur: 0 sum
Total: 9,000,000 sum

The loop does not need if statements to check the specific type of each employee. It simply calls calculate_pay(), and each object applies its own formula. This is a major advantage of polymorphism: new employee types may be added without changing the payroll loop itself.

For comparison, a non-object-oriented approach often relies on explicit type checks:

# The old, painful way
for emp in employees:
    if emp["type"] == "full_time":
        pay = emp["monthly_salary"]
    elif emp["type"] == "part_time":
        pay = emp["hours_worked"] * emp["hourly_rate"]
    elif emp["type"] == "intern":
        pay = 0
    # ... what if we add 10 more types?

In this style, every new type requires another elif branch. Polymorphism improves extensibility by allowing new types to be introduced without modifying existing processing logic.


Duck Typing — “If It Quacks Like a Duck…”

Python supports a particularly flexible form of polymorphism known as duck typing. In some languages, such as Java or C++, polymorphism is usually tied to inheritance from a shared parent class. Python is less strict. It focuses not on what an object is, but on what an object can do.

This idea is summarized by the phrase:

“If it walks like a duck and quacks like a duck, then it must be a duck.”

In programming terms, Python only needs the object to provide the required method. If the method exists, Python uses it. If it does not exist, an error occurs at runtime.

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

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

class Robot:
    def speak(self):
        return "BEEP BOOP"

These classes do not share a custom parent class, yet they all define speak().

def make_it_speak(thing):
    print(thing.speak())

make_it_speak(Dog())    # Woof!
make_it_speak(Cat())    # Meow!
make_it_speak(Robot())  # BEEP BOOP

The function make_it_speak() does not check the type of thing. It simply calls .speak(). Any object with that method can be used successfully.

The same principle appears in more practical situations. Consider a function that processes data from different sources:

class FileSource:
    def __init__(self, filename):
        self.filename = filename

    def read_data(self):
        with open(self.filename) as f:
            return f.read()

class DatabaseSource:
    def __init__(self, query):
        self.query = query

    def read_data(self):
        # pretend we're querying a database
        return f"Results for: {self.query}"

class APISource:
    def __init__(self, url):
        self.url = url

    def read_data(self):
        # pretend we're fetching from an API
        return f"Data from {self.url}"
def process(source):
    data = source.read_data()
    print(f"Processing: {data}")

process(FileSource("data.txt"))
process(DatabaseSource("SELECT * FROM students"))
process(APISource("https://api.example.com/grades"))

The process() function works with any object that provides a read_data() method. The exact source of the data is irrelevant to the function, which illustrates the flexibility of duck typing.


The Problem with Duck Typing

Although duck typing is flexible and expressive, it also introduces a risk: missing methods are detected only when the relevant code is executed.

class CSVSource:
    def __init__(self, filename):
        self.filename = filename

    # oops, forgot read_data()!

process(CSVSource("grades.csv"))
AttributeError: 'CSVSource' object has no attribute 'read_data'

The error appears only at runtime, when process() actually attempts to call read_data(). In small programs this may be acceptable, but in larger systems it can delay the discovery of design mistakes. This motivates the use of a stricter mechanism when stronger guarantees are needed.


Abstract Base Classes (ABCs)

An abstract base class is a class that has two essential characteristics:

  1. It cannot be instantiated directly.
  2. It requires subclasses to implement specific methods.

An abstract base class acts as a contract. It defines what operations a subclass must provide, without necessarily providing the implementation itself.

Python supports this mechanism through the abc module:

from abc import ABC, abstractmethod

class DataSource(ABC):
    @abstractmethod
    def read_data(self):
        pass

The components have the following roles:

  • ABC is the base class used to mark a class as abstract.
  • @abstractmethod identifies methods that subclasses must override.
  • pass is used because the abstract class declares the method but does not implement it.

If an attempt is made to instantiate the abstract class directly, Python rejects it:

source = DataSource()
TypeError: Can't instantiate abstract class DataSource
with abstract method read_data

The class functions as a blueprint rather than a finished object. Concrete subclasses must provide the required behavior:

class FileSource(DataSource):
    def __init__(self, filename):
        self.filename = filename

    def read_data(self):
        with open(self.filename) as f:
            return f.read()

class APISource(DataSource):
    def __init__(self, url):
        self.url = url

    def read_data(self):
        return f"Data from {self.url}"

These subclasses can be instantiated because they implement read_data().

If a subclass fails to implement the required method, the problem is detected immediately:

class BrokenSource(DataSource):
    def __init__(self):
        pass

    # forgot read_data()!

b = BrokenSource()
TypeError: Can't instantiate abstract class BrokenSource
with abstract method read_data

This is the main contrast with duck typing. Duck typing postpones failure until the method call occurs, whereas an abstract base class enforces the contract at object creation time.

  Duck Typing Abstract Base Class
Error if method missing At runtime, when method is called At instantiation, when object is created
Inheritance required? No Yes (must inherit from ABC)
Philosophy “Try it and see” “Prove it upfront”

Neither approach is universally superior. Duck typing is often suitable for small and flexible code, whereas ABCs are particularly useful in larger systems where structure and early error detection are important.


Building a Real Example with ABCs

The relationship between abstraction and polymorphism becomes clearer in a larger example. Consider the following shape hierarchy:

from abc import ABC, abstractmethod
import math

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

    @abstractmethod
    def perimeter(self):
        pass

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

This abstract class contains two abstract methods, area() and perimeter(), and one regular method, describe(). An abstract class may therefore combine required behavior with shared implemented behavior. The describe() method relies on area() and perimeter(), and this is safe because Python prevents instantiation of incomplete subclasses.

Concrete subclasses may then supply the required formulas:

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

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

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

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

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

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

Because both classes implement all abstract methods, they may be instantiated and used polymorphically:

shapes = [Circle(5), Rectangle(4, 6), Circle(3)]

for shape in shapes:
    print(shape.describe())
Circle: area=78.54, perimeter=31.42
Rectangle: area=24.00, perimeter=20.00
Circle: area=28.27, perimeter=18.85

The loop treats all objects uniformly as shapes, while each object computes its own area and perimeter according to its specific class.

If a subclass is incomplete, the error is reported immediately:

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

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

    # forgot perimeter()!

t = Triangle(3, 4)
TypeError: Can't instantiate abstract class Triangle
with abstract method perimeter

Thus, the ABC protects the design by enforcing the required interface.


What Is an “Interface”?

In object-oriented programming, the term interface usually refers to a structure that specifies what methods an object must provide, without defining how those methods are implemented. Some languages, such as Java, include a dedicated interface keyword. Python does not have a separate keyword for this purpose, but the same idea can be represented with abstract base classes in which all methods are abstract.

from abc import ABC, abstractmethod

class Printable(ABC):
    @abstractmethod
    def to_string(self):
        pass

class Saveable(ABC):
    @abstractmethod
    def save(self, filename):
        pass

These abstract classes define contracts. Any class that inherits from Printable must implement to_string(), and any class that inherits from Saveable must implement save().

A single class may implement multiple such interfaces:

class Report(Printable, Saveable):
    def __init__(self, title, content):
        self.title = title
        self.content = content

    def to_string(self):
        return f"Report: {self.title}\n{self.content}"

    def save(self, filename):
        with open(filename, "w") as f:
            f.write(self.to_string())

This is a practical use of multiple inheritance. The class Report satisfies both contracts.

r = Report("Midterm Results", "Average score: 78%")
print(r.to_string())
r.save("report.txt")

print(isinstance(r, Printable))  # True
print(isinstance(r, Saveable))   # True

This design pattern is common in Python. Small, focused abstract classes make program structure explicit and allow concrete classes to combine responsibilities as needed.


Duck Typing vs ABCs — When to Use Which?

The two approaches serve different practical purposes.

Duck typing is useful when:

  • the program is small or personal;
  • flexibility is more important than strict structure;
  • minimal boilerplate is preferred;
  • developers are comfortable relying on convention.
# Duck typing: just call it
def export(exporter):
    exporter.export()  # works if export() exists, crashes if not

Abstract base classes are useful when:

  • building frameworks or libraries for other users;
  • early error detection is important;
  • many interchangeable implementations are expected;
  • the contract should be explicit and self-documenting.
# ABC: enforced contract
class Exporter(ABC):
    @abstractmethod
    def export(self):
        pass

In practice, Python developers often use duck typing for everyday code and ABCs for important architectural boundaries. Python itself uses abstract base classes in collections.abc, which includes abstractions such as Iterable, Sequence, and Mapping.


Putting It All Together

The following example combines abstraction and polymorphism in a notification system:

from abc import ABC, abstractmethod

class Notifier(ABC):
    @abstractmethod
    def send(self, message):
        pass

class EmailNotifier(Notifier):
    def __init__(self, email):
        self.email = email

    def send(self, message):
        print(f"Sending email to {self.email}: {message}")

class SMSNotifier(Notifier):
    def __init__(self, phone):
        self.phone = phone

    def send(self, message):
        print(f"Sending SMS to {self.phone}: {message}")

class TelegramNotifier(Notifier):
    def __init__(self, chat_id):
        self.chat_id = chat_id

    def send(self, message):
        print(f"Sending Telegram to {self.chat_id}: {message}")

The function that sends notifications is independent of the specific notification type:

def notify_all(notifiers, message):
    for notifier in notifiers:
        notifier.send(message)
team = [
    EmailNotifier("alisher@akhu.uz"),
    SMSNotifier("+998901234567"),
    TelegramNotifier("@sevara_dev"),
]

notify_all(team, "Server is down!")
Sending email to alisher@akhu.uz: Server is down!
Sending SMS to +998901234567: Server is down!
Sending Telegram to @sevara_dev: Server is down!

If a SlackNotifier is added later, the notify_all() function does not need to change. It only depends on the shared interface send(). This demonstrates the central idea of the lecture: one interface may take many forms.

At the same time, if a new notifier class inherits from Notifier but fails to implement send(), the abstract base class will detect the error immediately. In this way, polymorphism provides flexibility, while abstraction provides structure and safety.

This lecture therefore establishes three central ideas: polymorphism allows uniform method calls across different objects, duck typing supports flexible behavior based on capabilities rather than explicit type, and abstract base classes enforce required interfaces when stronger guarantees are needed.