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ΒΆ
- Theory Session - Inheritance and Polymorphism Fundamentals
- Hands-on Exercise - Designing Class Hierarchies
- Pattern Deep-Dive - When to use inheritance vs. composition
- Live Demonstration - Polymorphic Interfaces using ABCs
- 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ΒΆ
- Contract enforcement - Subclasses must implement required methods
- Polymorphic design - Write code that works with any Employee subclass
- Type safety - IDEs can warn if you forget to implement something
- 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.pywith 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 β