Skip to content

Session 6: Inheritance & PolymorphismΒΆ

Week: 6
Element: ICTPRG430 Element 2.2
Duration: 4 hours
Phase: Object-Oriented Programming Theory


Session IntroductionΒΆ

In this session, you'll deepen your understanding of Object-Oriented Programming by exploring inheritance and polymorphism. Building on Session 4's class fundamentals and Session 5's composition patterns, you'll now learn how to create specialized types through the "is-a" relationship. You'll understand when to use inheritance versus composition, and how both work together to create flexible, maintainable systems. Through practical real-world examples and exercises, you'll design hierarchies and implement polymorphic interfaces that extend naturally to any domain.

Learning ObjectivesΒΆ

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

  • Understand and implement class inheritance hierarchies for specialized types
  • Apply method overriding and the super() function appropriately
  • Implement polymorphic interfaces for code extensibility and flexible behaviors
  • Analyze and apply the "is-a" vs. "has-a" relationships, favoring composition over inheritance in design
  • Design and utilize abstract base classes to define clear contracts for related classes

Session StructureΒΆ

  1. Theory Session - Inheritance and Polymorphism Fundamentals
  2. Hands-on Exercise - Designing Class Hierarchies
  3. Pattern Deep-Dive - When to use inheritance vs. composition
  4. Live Demonstration - Polymorphic Interfaces using ABCs
  5. Extension Activity - Understanding Multiple Inheritance

Pre-Session PreparationΒΆ

Required

  • Python 3.9+ installed
  • VS Code with Python extension
  • Completed Session 4 (Class fundamentals and encapsulation)
  • Completed Session 5 (Composition and component design)

Review

  • Session 4: Classes, objects, @property, magic methods
  • Session 5: Composition patterns, "has-a" relationships
  • Understanding the difference between composition and inheritance

---ΒΆ

πŸ“Š Lecture SlidesΒΆ

Week 6 Presentation Slides

View the presentation slides for this session:

πŸ“Š Open Slides in New Window

The slides contain: - Inheritance and the "is-a" relationship - The super() function and method overriding - Polymorphism and multiple types with same interface - Abstract base classes (ABC) and contracts - Design patterns: when to use inheritance vs. composition - Liskov Substitution Principle and type safety - Assessment connection (AT2 Part A requirements)

1. Inheritance BasicsΒΆ

Inheritance allows you to create a new class that extends an existing one. The new class inherits all methods and attributes from the parent.

The "Is-A" RelationshipΒΆ

Consider a company with different employee types. All employees share core attributes and behaviors, but each type may have specialized ones:

classDiagram
    class Employee {
        -name: str
        -salary: float
        +get_annual_salary() float
    }

    class Manager {
        -team_size: int
        +calculate_bonus() float
    }

    class Developer {
        -languages: list
        +calculate_bonus() float
    }

    Employee <|-- Manager
    Employee <|-- Developer

Now let's implement this:

class Employee:
    """Base employee class with core functionality."""

    def __init__(self, name: str, salary: float):
        self.name = name
        self.salary = salary

    def get_annual_salary(self) -> float:
        """Return annual salary."""
        return self.salary

class Manager(Employee):
    """Manager specializes employee with team management."""

    def __init__(self, name: str, salary: float, team_size: int):
        super().__init__(name, salary)
        self.team_size = team_size

    def calculate_bonus(self) -> float:
        """Managers get bonus based on team size."""
        return self.salary * 0.1 * (self.team_size / 10)

Understanding super()ΒΆ

The super() function calls the parent class's method. This lets you reuse parent code without duplication:

manager = Manager("Alice", 80000, 5)
print(manager.name)           # Inherited from Employee
print(manager.team_size)      # Added in Manager
print(manager.get_annual_salary())  # Uses Employee's method
print(manager.calculate_bonus())    # Uses Manager's method

2. PolymorphismΒΆ

Polymorphism means "many forms." Different employee types can respond to the same method call differently.

Example: Calculating BonusesΒΆ

Without polymorphism (tedious if-statements):

def pay_bonus(employee):
    if isinstance(employee, Manager):
        bonus = employee.salary * 0.1
    elif isinstance(employee, Developer):
        bonus = employee.salary * 0.05
    else:
        bonus = 0
    return bonus

With polymorphism (clean, extensible):

classDiagram
    class Employee {
        <<abstract>>
        +calculate_bonus() float*
    }

    class Manager {
        +calculate_bonus() float
    }

    class Developer {
        +calculate_bonus() float
    }

    class Consultant {
        +calculate_bonus() float
    }

    Employee <|-- Manager
    Employee <|-- Developer
    Employee <|-- Consultant

The diagram shows how each subclass overrides calculate_bonus() with its own implementation. When code calls employee.calculate_bonus(), the actual method depends on the runtime type.

def pay_bonus(employee: Employee) -> float:
    """Pay bonus based on employee type - no type checking needed."""
    return employee.calculate_bonus()

class Manager(Employee):
    def calculate_bonus(self) -> float:
        """Manager bonus: 10% base."""
        return self.salary * 0.1

class Developer(Employee):
    def calculate_bonus(self) -> float:
        """Developer bonus: 5% base."""
        return self.salary * 0.05

class Consultant(Employee):
    def calculate_bonus(self) -> float:
        """Consultant bonus: 2% base."""
        return self.salary * 0.02

3. Method OverridingΒΆ

When a child class defines its own version of a parent's method, it "overrides" that method. This allows different types to behave differently while maintaining a common interface.

classDiagram
    class Employee {
        +move_forward(speed) void
    }

    class Manager {
        +move_forward(speed) void
        note: Parent implementation
    }

    class Developer {
        +move_forward(speed) void
        note: Child override
    }

    Employee <|-- Manager
    Employee <|-- Developer
class Developer(Employee):
    """Developer specializes employee with coding skills."""

    def __init__(self, name: str, salary: float, languages: list):
        super().__init__(name, salary)
        self.languages = languages

    def calculate_bonus(self) -> float:
        """Developer bonus depends on experience."""
        base_bonus = self.salary * 0.05
        language_bonus = len(self.languages) * 500
        return base_bonus + language_bonus

class Manager(Employee):
    """Manager specializes employee with team leadership."""

    def __init__(self, name: str, salary: float, team_size: int):
        super().__init__(name, salary)
        self.team_size = team_size

    def calculate_bonus(self) -> float:
        """Manager bonus depends on team size."""
        return self.salary * 0.1 * (self.team_size / 10)

Now different employees calculate bonuses their own way:

developer = Developer("Bob", 75000, ["Python", "JavaScript"])
manager = Manager("Alice", 80000, 5)

print(developer.calculate_bonus())  # Uses Developer logic
print(manager.calculate_bonus())    # Uses Manager logic

4. Abstract Base ClassesΒΆ

An abstract base class defines an interface that subclasses must implement. Use the abc module:

from abc import ABC, abstractmethod

class Employee(ABC):
    """Abstract base class for all employees."""

    def __init__(self, name: str, salary: float):
        self.name = name
        self.salary = salary

    @abstractmethod
    def get_annual_salary(self) -> float:
        """Every employee type must report their salary."""
        pass

    @abstractmethod
    def calculate_bonus(self) -> float:
        """Every employee type must calculate bonus their way."""
        pass

    def display_info(self) -> str:
        """Generic method - can be overridden but doesn't have to be."""
        return f"{self.name}: ${self.salary}/year"

ABC Example: Concrete ImplementationΒΆ

classDiagram
    class Employee {
        <<abstract>>
        -name: str
        -salary: float
        +get_annual_salary()* float
        +calculate_bonus()* float
    }

    class Manager {
        -team_size: int
        +get_annual_salary() float
        +calculate_bonus() float
    }

    class Developer {
        -languages: list
        +get_annual_salary() float
        +calculate_bonus() float
    }

    Employee <|-- Manager
    Employee <|-- Developer
class Manager(Employee):
    """Must implement all abstract methods."""

    def __init__(self, name: str, salary: float, team_size: int):
        super().__init__(name, salary)
        self.team_size = team_size

    def get_annual_salary(self) -> float:
        """Manager annual salary."""
        return self.salary

    def calculate_bonus(self) -> float:
        """Manager bonus based on team."""
        return self.salary * 0.1 * (self.team_size / 10)

class Developer(Employee):
    """Must implement all abstract methods."""

    def __init__(self, name: str, salary: float, languages: list):
        super().__init__(name, salary)
        self.languages = languages

    def get_annual_salary(self) -> float:
        """Developer annual salary."""
        return self.salary

    def calculate_bonus(self) -> float:
        """Developer bonus based on skills."""
        base_bonus = self.salary * 0.05
        language_bonus = len(self.languages) * 500
        return base_bonus + language_bonus

# This works:
manager = Manager("Alice", 80000, 5)
developer = Developer("Bob", 75000, ["Python", "JavaScript"])

# This fails (missing implementations):
# employee = Employee("Generic", 50000)  # TypeError: Can't instantiate abstract class

Benefits of Abstract ClassesΒΆ

  1. Contract enforcement - Subclasses must implement required methods
  2. Polymorphic design - Write code that works with any Employee subclass
  3. Type safety - IDEs can warn if you forget to implement something
  4. Documentation - Abstract methods document the interface

5. Design Patterns & Best PracticesΒΆ

When to Use Inheritance vs. CompositionΒΆ

Use inheritance ("is-a"): - Manager IS-A Employee - Developer IS-A Employee - SportsCar IS-A Car

Use composition ("has-a"): - Employee HAS-A Computer (not Employee IS-A Computer) - Car HAS-A Engine (not Car IS-A Engine) - Robot HAS-A Sensor (not Robot IS-A Sensor)

Liskov Substitution Principle (LSP)ΒΆ

Subclasses must be usable wherever their parent is used:

# ❌ BAD: Violates LSP
class Employee:
    def get_annual_salary(self) -> float:
        return self.salary  # Always positive

class WeirdPosition(Employee):
    def get_annual_salary(self) -> float:
        return -self.debt  # Returns negative!

This breaks LSP. Code expecting Employee to have positive salary will fail. Don't do this.

Safe Type CheckingΒΆ

When you need type-specific behavior, use isinstance() carefully:

# OK: Using isinstance for dispatch
def describe_employee(employee: Employee) -> str:
    if isinstance(employee, Manager):
        return f"{employee.name}, Manager with {employee.team_size} reports"
    elif isinstance(employee, Developer):
        return f"{employee.name}, Developer skilled in {', '.join(employee.languages)}"
    else:
        return f"{employee.name}, Employee"

# Better: Use polymorphism instead
class Employee(ABC):
    @abstractmethod
    def describe(self) -> str:
        pass

# Then every employee type implements describe()

6. Assessment ConnectionΒΆ

This session directly supports AT2-Session-6-7 Part A: Inheritance & Polymorphism (65%).

What You're Building TowardΒΆ

In the assessment, you'll apply these OOP patterns to create specialized types: - Inheritance hierarchy with 3+ specialized classes - Proper use of super() and method overriding - Abstract base class defining the interface - Clean polymorphic design with no type-checking

Google Docstring FormatΒΆ

This course requires Google-style docstrings for all classes and public methods.

Format:

def method_name(param1: type, param2: type) -> return_type:
    """Short one-line summary of what this does.

    Longer description if needed, explaining the purpose,
    behavior, and any important details.

    Args:
        param1: Description of param1
        param2: Description of param2

    Returns:
        Description of what is returned

    Raises:
        ExceptionType: When this exception is raised
    """

Example:

class Manager(Employee):
    """Represents a manager with team leadership responsibilities.

    Managers earn bonuses based on their team size and can manage
    multiple employees.
    """

    def __init__(self, name: str, salary: float, team_size: int):
        """Initialize a manager.

        Args:
            name: Manager's full name
            salary: Annual salary in dollars
            team_size: Number of direct reports
        """
        super().__init__(name, salary)
        self.team_size = team_size

    def calculate_bonus(self) -> float:
        """Calculate manager's annual bonus.

        Bonus is 10% of salary multiplied by team size factor
        (team_size / 10).

        Returns:
            Bonus amount in dollars

        Example:
            >>> mgr = Manager("Alice", 80000, 5)
            >>> mgr.calculate_bonus()
            4000.0
        """
        return self.salary * 0.1 * (self.team_size / 10)

Part A RequirementsΒΆ

  • File: Enhanced robot_base.py with inheritance hierarchy
  • Classes: 3+ specialized types (inherit from base class)
  • Abstract methods: Core behaviors must be abstract and implemented by subclasses
  • Google docstrings: On all classes and public methods
  • No simulator code: Pure OOP design
  • Polymorphic behavior: Different types respond differently to same method calls

Check Your KnowledgeΒΆ

Q1: What does super() do?

Why call super().__init__() in a subclass?

Answer

It runs the parent class's __init__ to set up inherited attributes. Without it, parent's __init__ code is skipped.

Q2: When to override methods?

If a parent method works fine, should you override it?

Answer

No, unless the child needs different behavior. Override only when: - You need to add child-specific logic - The parent's default doesn't fit this type

Q3: Abstract methods

Can you instantiate a class if it has abstract methods?

Answer

No. You can't instantiate an abstract class. Only concrete subclasses that implement all abstract methods can be instantiated.

Q4: Polymorphism benefit

Why write code for a parent class instead of specific subclasses?

Answer

Your code then works with ANY type, current or future. Add new subclasses without changing existing code.


Next Session PreviewΒΆ

Week 7: Refactoring to OOP & Custom Exceptions - Apply Sessions 4-6 to refactor real-world messy code - Design clean class hierarchies using composition and inheritance - Implement custom exception hierarchies for robust error handling - Make IS-A vs HAS-A design decisions - Prepare code for packaging and distribution - Coffee shop ordering system refactoring exercise


Navigation:
← Week 5 | Learning Plan | Week 7 β†’