Hello! Welcome to your next lesson in the Python Foundations module.
In our last session, you learned how to build a class from scratch, complete with an __init__ constructor, attributes to hold data, and methods to define behavior. This was your first step into Object-Oriented Programming (OOP), allowing you to model real-world concepts as self-contained objects like a Student.
Today, we'll build on that foundation to explore how classes can be related to each other. You'll learn how to implement inheritance and demonstrate polymorphism with method overriding. These concepts are crucial for writing code that is organized, reusable, and scalable—essential skills for building the complex systems required in AI development.
1. The "Is A" Relationship: Inheritance
Inheritance is a fundamental OOP concept that allows you to create a new class that inherits the attributes and methods of an existing class. The new class is called a subclass (or child class), and the existing class is the superclass (or parent class).
This models an "is a" relationship. For example, a Car is a Vehicle, and a Dog is an Animal. The subclass is a more specialized version of the superclass. It gets all the functionality of the parent for free and can then add its own unique features.
Inheritance and Composition: A Python OOP Guide
To start, let's get a solid conceptual and practical understanding from the Real Python guide. This text will walk you through creating a hierarchy of employee classes, which is a classic and very clear example of inheritance.
Please read the section titled 'Creating Class Hierarchies'. Focus on how the SalaryEmployee, HourlyEmployee, and CommissionEmployee classes all inherit from the base Employee class. Notice the syntax class SalaryEmployee(Employee): and the use of super().__init__(...) to call the parent's constructor.
The key idea is code reuse. Instead of defining id and name in every single employee class, you define them once in the Employee parent class, and all child classes automatically inherit them.
2. Specializing Behavior: Method Overriding
What happens when a subclass needs to perform an action differently than its parent? For instance, a Dog and a Cat both make a sound, but they do it differently. This is where method overriding comes in.
A subclass can provide a specific implementation for a method that is already defined in its superclass.

Let's watch a video that demonstrates inheritance and method overriding with a Vehicle example.
Python Classes, Objects, Inheritance & Polymorphism for Beginners
This video from Dave Gray provides a very clear, hands-on demonstration of creating a parent Vehicle class and then subclasses like Airplane and Truck that override the moves() method.
Watch from 07:47 to 12:12. Pay attention to: How the Airplane and Truck classes are defined to inherit from Vehicle. How they each define their own moves() method, which overrides the parent's moves() method. How the GolfCart class doesn't override moves(), so it uses the parent's version by default.
3. The Power of Polymorphism
Now we get to the "why." Why are inheritance and method overriding so useful? They enable polymorphism.
The word "polymorphism" comes from Greek and means "many forms." In programming, it's the ability to use a single interface to represent different underlying forms (data types). In simpler terms, you can call the same method on different objects, and each object will respond in its own way.
For example, you can have a list of different Animal objects (Dog, Cat, Bird) and simply call .speak() on each one. Polymorphism ensures that the correct sound is produced for each animal without you needing to write if/else statements to check the animal's type.

The video you just watched has a perfect demonstration of this.
Python Classes, Objects, Inheritance & Polymorphism for Beginners
Let's continue with the same video from Dave Gray. The next section explains polymorphism by creating a list of different vehicle objects and calling the same methods on them in a loop.
Watch from 15:28 to 18:23. Notice how the for loop treats every object simply as a 'vehicle' (v) and calls v.moves(). However, the output is different and specific to the actual class of the object (Airplane, Truck, etc.). This is the essence of polymorphism.
4. Extending the Parent: Using super() in the Constructor
Often, a subclass needs to add new attributes that the parent class doesn't have. For this, the subclass needs its own __init__ method.
When you define an __init__ method in a subclass, it overrides the parent's __init__. To avoid rewriting the logic from the parent's constructor, you can call it directly from the child's constructor using the super() function.
super() gives you access to methods in the parent class. super().__init__(...) is a common pattern to ensure the parent class is properly initialized before adding the subclass-specific attributes.
Python Classes, Objects, Inheritance & Polymorphism for Beginners
Let's watch the final relevant clip from the Dave Gray video, where he explains exactly how and why to use super() when a subclass needs additional attributes.
Watch from 12:12 to 15:28. The example shows adding an FAA_ID to the Airplane class. The key takeaway is the line super().__init__(make, model), which handles the initialization of the attributes inherited from Vehicle.
5. Practice: Employee Payroll System
Now it's your turn to put it all together. You'll create a simple payroll system using inheritance and polymorphism, similar to the hr.py example from the Real Python article.
Instructions:
- Create a base class named
Employeewith an__init__method that acceptsnameandid. - Create a
SalaryEmployeeclass that inherits fromEmployee.- Its
__init__should acceptname,id, andweekly_salary. It should usesuper()to initialize thenameandid. - It should have a method
calculate_payroll()that returns theweekly_salary.
- Its
- Create an
HourlyEmployeeclass that inherits fromEmployee.- Its
__init__should acceptname,id,hours_worked, andhourly_rate. It should usesuper()to initializenameandid. - It should override the
calculate_payroll()method to returnhours_worked * hourly_rate.
- Its
- Create instances of each subclass and test that calling
calculate_payroll()on them returns the correct amount.
# Your code here
class Employee:
# TODO: Implement the base Employee class
pass
class SalaryEmployee(Employee):
# TODO: Implement the SalaryEmployee subclass
pass
class HourlyEmployee(Employee):
# TODO: Implement the HourlyEmployee subclass
pass
# --- Testing your classes ---
# salary_emp = SalaryEmployee("John Doe", 1, 1500)
# hourly_emp = HourlyEmployee("Jane Smith", 2, 40, 25)
# print(f"{salary_emp.name}'s payroll: ${salary_emp.calculate_payroll()}")
# print(f"{hourly_emp.name}'s payroll: ${hourly_emp.calculate_payroll()}")
Click here to see a possible solution
class Employee:
"""Base class for all employees."""
def __init__(self, name, id):
self.name = name
self.id = id
def calculate_payroll(self):
"""Placeholder for payroll calculation. Subclasses should override this."""
return 0
class SalaryEmployee(Employee):
"""An employee who is paid a fixed weekly salary."""
def __init__(self, name, id, weekly_salary):
super().__init__(name, id)
self.weekly_salary = weekly_salary
def calculate_payroll(self):
"""Returns the fixed weekly salary."""
return self.weekly_salary
class HourlyEmployee(Employee):
"""An employee who is paid by the hour."""
def __init__(self, name, id, hours_worked, hourly_rate):
super().__init__(name, id)
self.hours_worked = hours_worked
self.hourly_rate = hourly_rate
def calculate_payroll(self):
"""Calculates payroll based on hours worked."""
return self.hours_worked * self.hourly_rate
# --- Testing your classes ---
salary_emp = SalaryEmployee("John Doe", 1, 1500)
hourly_emp = HourlyEmployee("Jane Smith", 2, 40, 25)
print(f"{salary_emp.name}'s payroll: ${salary_emp.calculate_payroll()}")
# Expected output: John Doe's payroll: $1500
print(f"{hourly_emp.name}'s payroll: ${hourly_emp.calculate_payroll()}")
# Expected output: Jane Smith's payroll: $1000
# Polymorphism demonstration
employees = [salary_emp, hourly_emp]
print("\nCalculating payroll for all employees:")
for employee in employees:
print(f"- Payroll for {employee.name}: ${employee.calculate_payroll()}")
Conclusion
Great job! You've successfully navigated two of the most powerful concepts in Object-Oriented Programming. Building class hierarchies allows you to create logical, structured, and highly reusable code.
Let's summarize the key takeaways:
- Inheritance models an "is a" relationship, allowing a subclass to inherit attributes and methods from a superclass.
- Method Overriding allows a subclass to provide its own unique implementation of a method inherited from its parent.
- Polymorphism is the result of this: you can call the same method on objects of different classes and get different, class-specific behavior.
- The
super()function is used to call methods from the parent class, which is especially useful for extending the parent's__init__constructor.
In our next lesson, we will cover exception handling. You'll learn how to gracefully manage errors in your programs using try-except blocks. You will also see a direct application of today's topic when you learn to create custom exceptions, which are classes that inherit from Python's built-in Exception class.