Week 5 Lecture: Polymorphism & Abstraction

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:
- It cannot be instantiated directly.
- 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:
ABCis the base class used to mark a class as abstract.@abstractmethodidentifies methods that subclasses must override.passis 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.
This content will be available starting March 06, 2026.