Skip to content

This textbook is in beta – content is actively being refined. Report issues or suggestions

5.3 Abstraction, generalisation, inheritance, polymorphism

Learning objectives

By the end of this section, you will be able to:

  • Explain the purpose of abstraction in object-oriented programming

  • Use generalisation as a modelling tool to identify common patterns

  • Understand inheritance for code reuse and creating class hierarchies

  • Describe polymorphism through interfaces and duck typing

  • Apply these concepts to create flexible and maintainable code

Abstraction: Hiding complexity

Abstraction is about hiding unnecessary details and exposing only what’s essential. When you drive a car, you don’t need to understand how the engine works internally - you just use the steering wheel, pedals, and gear stick. This is abstraction in action.

In programming, abstraction helps us:

  • Manage complexity by hiding implementation details

  • Focus on what an object does rather than how it does it

  • Create simple interfaces for complex operations

class FileManager:
    """Abstract file operations - users don't need to know about
    file handles, buffer sizes, or error codes"""

    def save_data(self, filename, data):
        """Simple interface hides complex file operations"""
        try:
            with open(filename, 'w') as file:
                # Complex operations hidden here:
                # - Opening file handles
                # - Buffer management
                # - Error handling
                # - Resource cleanup
                file.write(data)
            return "File saved successfully"
        except Exception as e:
            return f"Error saving file: {str(e)}"

    def load_data(self, filename):
        """Another simple interface for complex operations"""
        try:
            with open(filename, 'r') as file:
                return file.read()
        except FileNotFoundError:
            return None
        except Exception as e:
            return f"Error: {str(e)}"

# Usage: simple and clean, complexity is hidden
file_manager = FileManager()
file_manager.save_data("notes.txt", "My important notes")
content = file_manager.load_data("notes.txt")

Generalisation: Finding common patterns

Generalisation is the process of identifying common characteristics and behaviors across different objects, then creating a general model that captures these similarities.

Think about vehicles: cars, motorcycles, trucks, and bicycles are all different, but they share common characteristics (wheels, steering, movement) and behaviors (start, stop, turn).

# Before generalisation: separate, similar classes
class Car:
    def __init__(self, make, model):
        self.make = make
        self.model = model
        self.speed = 0

    def start_engine(self):
        return f"{self.make} {self.model} engine started"

    def accelerate(self):
        self.speed += 10
        return f"Car accelerating, speed: {self.speed}"

class Motorcycle:
    def __init__(self, make, model):
        self.make = make
        self.model = model
        self.speed = 0

    def start_engine(self):
        return f"{self.make} {self.model} engine started"

    def accelerate(self):
        self.speed += 15
        return f"Motorcycle accelerating, speed: {self.speed}"

# After generalisation: common base class
class Vehicle:
    """General vehicle class capturing common characteristics"""

    def __init__(self, make, model, vehicle_type):
        self.make = make
        self.model = model
        self.vehicle_type = vehicle_type
        self.speed = 0

    def start_engine(self):
        return f"{self.make} {self.model} engine started"

    def accelerate(self, increment=10):
        self.speed += increment
        return f"{self.vehicle_type} accelerating, speed: {self.speed}"

    def stop(self):
        self.speed = 0
        return f"{self.vehicle_type} stopped"

Inheritance: Building on existing code

Inheritance allows us to create new classes based on existing ones, inheriting their attributes and methods while adding new features or modifying existing ones.

class Vehicle:
    """Base class for all vehicles"""

    def __init__(self, make, model):
        self.make = make
        self.model = model
        self.speed = 0
        self.is_running = False

    def start(self):
        if not self.is_running:
            self.is_running = True
            return f"{self.make} {self.model} started"
        return "Already running"

    def stop(self):
        self.speed = 0
        self.is_running = False
        return f"{self.make} {self.model} stopped"

    def get_info(self):
        return f"{self.make} {self.model}"

class Car(Vehicle):
    """Car inherits from Vehicle and adds car-specific features"""

    def __init__(self, make, model, doors):
        super().__init__(make, model)  # Call parent constructor
        self.doors = doors  # Car-specific attribute

    def accelerate(self):
        """Car-specific acceleration"""
        if self.is_running:
            self.speed += 10
            return f"Car accelerating, speed: {self.speed} km/h"
        return "Start the car first"

    def open_doors(self):
        """Car-specific method"""
        return f"Opening {self.doors} doors"

class Motorcycle(Vehicle):
    """Motorcycle inherits from Vehicle with different characteristics"""

    def __init__(self, make, model, engine_size):
        super().__init__(make, model)
        self.engine_size = engine_size

    def accelerate(self):
        """Motorcycle accelerates differently than cars"""
        if self.is_running:
            self.speed += 20  # Motorcycles accelerate faster
            return f"Motorcycle accelerating, speed: {self.speed} km/h"
        return "Start the motorcycle first"

    def wheelie(self):
        """Motorcycle-specific method"""
        if self.speed > 30:
            return "Performing a wheelie!"
        return "Need more speed for a wheelie"

# Usage demonstrates inheritance
car = Car("Toyota", "Camry", 4)
motorcycle = Motorcycle("Honda", "CBR", 600)

print(car.start())           # Inherited method
print(car.accelerate())      # Overridden method
print(car.open_doors())      # Car-specific method

print(motorcycle.start())    # Inherited method
print(motorcycle.accelerate())  # Overridden method
print(motorcycle.wheelie())  # Motorcycle-specific method

Polymorphism: Same interface, different behavior

Polymorphism means “many forms.” It allows different objects to respond to the same message (method call) in their own way. In Python, this often works through “duck typing” - if it walks like a duck and quacks like a duck, treat it like a duck.

class Dog:
    def __init__(self, name):
        self.name = name

    def make_sound(self):
        return f"{self.name} says Woof!"

    def move(self):
        return f"{self.name} runs on four legs"

class Cat:
    def __init__(self, name):
        self.name = name

    def make_sound(self):
        return f"{self.name} says Meow!"

    def move(self):
        return f"{self.name} stalks silently"

class Bird:
    def __init__(self, name):
        self.name = name

    def make_sound(self):
        return f"{self.name} says Tweet!"

    def move(self):
        return f"{self.name} flies through the air"

def animal_concert(animals):
    """Polymorphism in action: same method call, different behaviors"""
    print("🎵 Animal Concert Starting! 🎵")
    for animal in animals:
        print(animal.make_sound())  # Same method name, different sounds
        print(animal.move())        # Same method name, different movements
        print("---")

# Create different animals
pets = [
    Dog("Buddy"),
    Cat("Whiskers"),
    Bird("Tweety"),
    Dog("Max")
]

# Polymorphism: each animal responds differently to the same method calls
animal_concert(pets)

# Duck typing example
def pet_interaction(pet):
    """This function works with any object that has make_sound() and move()"""
    print(f"Interacting with pet:")
    print(pet.make_sound())
    print(pet.move())

# Works with any "pet-like" object
pet_interaction(Dog("Rover"))
pet_interaction(Cat("Fluffy"))

Bringing it all together

These four concepts work together to create powerful, flexible code:

# Abstract base for all shapes
class Shape:
    """Abstract shape class demonstrating all four concepts"""

    def __init__(self, name):
        self.name = name

    def area(self):
        """Abstract method - must be implemented by subclasses"""
        raise NotImplementedError("Subclasses must implement area()")

    def perimeter(self):
        """Abstract method"""
        raise NotImplementedError("Subclasses must implement perimeter()")

    def describe(self):
        """Common behavior for all shapes"""
        return f"This is a {self.name}"

# Inheritance: specific shapes inherit from Shape
class Rectangle(Shape):
    def __init__(self, width, height):
        super().__init__("Rectangle")  # Generalisation
        self.width = width
        self.height = height

    def area(self):  # Polymorphism: different implementation
        return self.width * self.height

    def perimeter(self):  # Polymorphism: different implementation
        return 2 * (self.width + self.height)

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

    def area(self):  # Polymorphism: different implementation
        return 3.14159 * self.radius ** 2

    def perimeter(self):  # Polymorphism: different implementation
        return 2 * 3.14159 * self.radius

# Abstraction: simple function hides complexity of different shapes
def calculate_total_area(shapes):
    """Works with any shape objects - abstraction and polymorphism"""
    total = 0
    for shape in shapes:
        total += shape.area()  # Polymorphic method call
    return total

# Usage demonstrates all concepts working together
shapes = [
    Rectangle(5, 3),
    Circle(4),
    Rectangle(2, 8)
]

print("Shape calculations:")
for shape in shapes:
    print(f"{shape.describe()}")  # Inherited method
    print(f"Area: {shape.area()}")  # Polymorphic method
    print(f"Perimeter: {shape.perimeter()}")  # Polymorphic method
    print("---")

print(f"Total area of all shapes: {calculate_total_area(shapes)}")

Practice

Exercise 1: Identifying the concepts

Look at this code and identify examples of abstraction, generalisation, inheritance, and polymorphism:

class MediaPlayer:
    def __init__(self, name):
        self.name = name

    def play(self):
        return f"{self.name} is playing"

    def stop(self):
        return f"{self.name} stopped"

class AudioPlayer(MediaPlayer):
    def play(self):
        return f"🎵 {self.name} playing audio"

class VideoPlayer(MediaPlayer):
    def play(self):
        return f"🎬 {self.name} playing video"

def start_playlist(players):
    for player in players:
        print(player.play())

Task: Identify and explain each concept in this code.

Sample Solution

Abstraction: The MediaPlayer class abstracts the concept of media playback, hiding the complex details of how different media types are actually played.

Generalisation: MediaPlayer represents the general concept that both audio and video players share - they can play and stop media.

Inheritance: AudioPlayer and VideoPlayer inherit from MediaPlayer, getting the basic functionality while specializing for their media type.

Polymorphism: The start_playlist() function calls play() on different player types, and each responds with their own specific behavior (audio vs video playback).

Exercise 2: Design a class hierarchy

Design a class hierarchy for different types of employees in a company:

  • All employees have: name, employee_id, base_salary

  • All employees can: work(), get_info()

  • Managers can: assign_task(task, employee), hold_meeting()

  • Developers can: write_code(language), debug()

  • Designers can: create_design(type), review_design()

Task: Create the class hierarchy with proper inheritance and polymorphism.

Sample Solution
class Employee:
    """Base class for all employees"""

    def __init__(self, name, employee_id, base_salary):
        self.name = name
        self.employee_id = employee_id
        self.base_salary = base_salary

    def work(self):
        return f"{self.name} is working"

    def get_info(self):
        return f"Employee: {self.name} (ID: {self.employee_id})"

class Manager(Employee):
    def __init__(self, name, employee_id, base_salary):
        super().__init__(name, employee_id, base_salary)
        self.team = []

    def work(self):
        return f"Manager {self.name} is managing the team"

    def assign_task(self, task, employee):
        return f"{self.name} assigned '{task}' to {employee.name}"

    def hold_meeting(self):
        return f"{self.name} is holding a team meeting"

class Developer(Employee):
    def work(self):
        return f"Developer {self.name} is coding"

    def write_code(self, language):
        return f"{self.name} is writing {language} code"

    def debug(self):
        return f"{self.name} is debugging code"

class Designer(Employee):
    def work(self):
        return f"Designer {self.name} is designing"

    def create_design(self, design_type):
        return f"{self.name} is creating a {design_type} design"

    def review_design(self):
        return f"{self.name} is reviewing design mockups"
Exercise 3: Duck typing demonstration

Create three different classes that don’t inherit from each other but can all be used polymorphically because they implement the same interface (duck typing):

  • Each start(), process(), and finish() methodsclass should have:

  • Classes could be: WashingMachine, Dishwasher, Printer

Task: Demonstrate duck typing by creating a function that works with all three classes.

Sample Solution
class WashingMachine:
    def __init__(self, brand):
        self.brand = brand

    def start(self):
        return f"{self.brand} washing machine starting wash cycle"

    def process(self):
        return "Washing clothes with soap and water"

    def finish(self):
        return "Wash cycle complete, clothes are clean"

class Dishwasher:
    def __init__(self, brand):
        self.brand = brand

    def start(self):
        return f"{self.brand} dishwasher starting cleaning cycle"

    def process(self):
        return "Spraying dishes with hot soapy water"

    def finish(self):
        return "Dishes are clean and dry"

class Printer:
    def __init__(self, brand):
        self.brand = brand

    def start(self):
        return f"{self.brand} printer initializing"

    def process(self):
        return "Printing document pages"

    def finish(self):
        return "Document printed successfully"

def run_appliance(appliance):
    """Duck typing: works with any object that has start/process/finish"""
    print(appliance.start())
    print(appliance.process())
    print(appliance.finish())
    print("---")

# All three work with the same function despite no inheritance
appliances = [
    WashingMachine("Samsung"),
    Dishwasher("Bosch"),
    Printer("HP")
]

for appliance in appliances:
    run_appliance(appliance)

Recap

  • Abstraction hides complexity and exposes only essential features through simple interfaces

  • Generalisation identifies common patterns and characteristics to create general models

  • Inheritance enables code reuse by building new classes on existing ones

  • Polymorphism allows different objects to respond to the same interface in their own way

  • Duck typing in Python enables polymorphism without requiring formal inheritance

  • These concepts work together to create flexible, maintainable, and reusable code

These four pillars of object-oriented programming provide powerful tools for managing complexity, promoting code reuse, and creating systems that are easy to extend and maintain. Understanding how they work together is essential for effective object-oriented design.