Python & AI Tutorials Logo
Python Programming

32. Extending Classes with Inheritance and Polymorphism

In Chapter 30, we learned how to create our own classes to model real-world concepts. We built classes like BankAccount and Student that bundled data and behavior together. But what happens when you need to create a new class that's similar to an existing one, but with some differences or additions?

Inheritance is Python's mechanism for creating new classes based on existing ones. Instead of copying and pasting code, you can create a subclass that automatically gets all the attributes and methods from a parent class (also called a base class or superclass), and then add or modify what you need.

This chapter explores how inheritance lets you build hierarchies of related classes, how to customize inherited behavior, and how polymorphism allows different classes to be used interchangeably when they share common interfaces.

32.1) Creating Subclasses from Existing Classes

32.1.1) The Basic Syntax of Inheritance

Inheritance allows you to create a new class (called a subclass or child class) based on an existing class (called a parent class or base class). The subclass automatically inherits all methods and attributes from the parent class.

When you create a subclass, you specify the parent class in parentheses after the class name.

python
# Parent class
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"
 
# Subclass inheriting from Animal
class Dog(Animal):
    pass  # No additional code needed yet
 
# Create a Dog instance
buddy = Dog("Buddy")
print(buddy.speak())  # Output: Buddy makes a sound
print(buddy.name)     # Output: Buddy

Even though Dog has no code of its own (just pass), it inherits everything from Animal. The Dog class automatically has the __init__ method and the speak method from its parent.

Animal

+name

+init(name)

+speak()

Dog

32.1.2) Why Inheritance Matters

Inheritance solves a common programming problem: code duplication. Imagine you're building a system to manage different types of employees:

python
# Without inheritance - lots of duplication
class FullTimeEmployee:
    def __init__(self, name, employee_id, salary):
        self.name = name
        self.employee_id = employee_id
        self.salary = salary
    
    def get_info(self):
        return f"{self.name} (ID: {self.employee_id})"
 
class PartTimeEmployee:
    def __init__(self, name, employee_id, hourly_rate):
        self.name = name
        self.employee_id = employee_id
        self.hourly_rate = hourly_rate
    
    def get_info(self):
        return f"{self.name} (ID: {self.employee_id})"

Notice how name, employee_id, and get_info() are duplicated. With inheritance, we can eliminate this duplication:

python
# With inheritance - shared code in parent class
class Employee:
    def __init__(self, name, employee_id):
        self.name = name
        self.employee_id = employee_id
    
    def get_info(self):
        return f"{self.name} (ID: {self.employee_id})"
 
class FullTimeEmployee(Employee):
    def __init__(self, name, employee_id, salary):
        Employee.__init__(self, name, employee_id)  # Call parent's __init__
        # Note: We'll learn a better way to do this with super() in Section 32.3
        self.salary = salary
 
class PartTimeEmployee(Employee):
    def __init__(self, name, employee_id, hourly_rate):
        Employee.__init__(self, name, employee_id)  # Call parent's __init__
        self.hourly_rate = hourly_rate
 
# Both subclasses inherit get_info()
alice = FullTimeEmployee("Alice", "E001", 75000)
bob = PartTimeEmployee("Bob", "E002", 25)
 
print(alice.get_info())  # Output: Alice (ID: E001)
print(bob.get_info())    # Output: Bob (ID: E002)

Now the common attributes and methods live in Employee, and each subclass only defines what makes it unique.

32.1.3) Adding New Methods to Subclasses

Subclasses can add their own methods that the parent doesn't have:

python
class Vehicle:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
    
    def get_description(self):
        return f"{self.brand} {self.model}"
 
class Car(Vehicle):
    def __init__(self, brand, model, num_doors):
        Vehicle.__init__(self, brand, model)
        self.num_doors = num_doors
    
    def honk(self):  # New method specific to Car
        return "Beep beep!"
 
class Motorcycle(Vehicle):
    def __init__(self, brand, model, has_sidecar):
        Vehicle.__init__(self, brand, model)
        self.has_sidecar = has_sidecar
    
    def rev_engine(self):  # New method specific to Motorcycle
        return "Vroom vroom!"
 
# Each subclass has its parent's methods plus its own
my_car = Car("Toyota", "Camry", 4)
print(my_car.get_description())  # Output: Toyota Camry
print(my_car.honk())             # Output: Beep beep!
 
my_bike = Motorcycle("Harley", "Sportster", False)
print(my_bike.get_description())  # Output: Harley Sportster
print(my_bike.rev_engine())       # Output: Vroom vroom!

The Car class has both get_description() (inherited) and honk() (its own). The Motorcycle class has get_description() (inherited) and rev_engine() (its own).

32.1.4) Adding New Attributes to Subclasses

Subclasses can also add their own instance attributes. You typically do this in the subclass's __init__ method:

python
class BankAccount:
    def __init__(self, account_number, balance):
        self.account_number = account_number
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        return self.balance
 
class SavingsAccount(BankAccount):
    def __init__(self, account_number, balance, interest_rate):
        BankAccount.__init__(self, account_number, balance)
        self.interest_rate = interest_rate  # New attribute
    
    def apply_interest(self):  # New method using new attribute
        interest = self.balance * self.interest_rate
        self.balance += interest
        return interest
 
# SavingsAccount has all BankAccount attributes plus interest_rate
savings = SavingsAccount("SA001", 1000, 0.03)
savings.deposit(500)  # Inherited method
print(f"Balance: ${savings.balance}")  # Output: Balance: $1500
 
interest_earned = savings.apply_interest()
print(f"Interest earned: ${interest_earned:.2f}")  # Output: Interest earned: $45.00
print(f"New balance: ${savings.balance:.2f}")      # Output: New balance: $1545.00

The SavingsAccount has account_number and balance from BankAccount, plus its own interest_rate attribute.

32.1.5) Multiple Levels of Inheritance

Classes can inherit from classes that themselves inherit from other classes, creating an inheritance hierarchy:

python
class LivingThing:
    def __init__(self, name):
        self.name = name
    
    def is_alive(self):
        return True
 
class Animal(LivingThing):
    def __init__(self, name, species):
        LivingThing.__init__(self, name)
        self.species = species
    
    def move(self):
        return f"{self.name} is moving"
 
class Dog(Animal):
    def __init__(self, name, breed):
        Animal.__init__(self, name, "Dog")
        self.breed = breed
    
    def bark(self):
        return f"{self.name} says: Woof!"
 
# Dog inherits from Animal, which inherits from LivingThing
max_dog = Dog("Max", "Golden Retriever")
 
# Methods from all three levels work
print(max_dog.is_alive())  # Output: True (from LivingThing)
print(max_dog.move())      # Output: Max is moving (from Animal)
print(max_dog.bark())      # Output: Max says: Woof! (from Dog)
 
# Attributes from all three levels exist
print(max_dog.name)     # Output: Max (from LivingThing)
print(max_dog.species)  # Output: Dog (from Animal)
print(max_dog.breed)    # Output: Golden Retriever (from Dog)

LivingThing

+name

+is_alive()

Animal

+species

+move()

Dog

+breed

+bark()

Dog inherits from Animal, which inherits from LivingThing. This means Dog has access to methods and attributes from both parent classes.

32.2) Overriding Methods in Subclasses

32.2.1) What Method Overriding Means

Sometimes a subclass needs to change how an inherited method works. Method overriding means defining a method in the subclass with the same name as a method in the parent class. When you call that method on a subclass instance, Python uses the subclass's version instead of the parent's.

python
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"
 
class Dog(Animal):
    def speak(self):  # Override the parent's speak method
        return f"{self.name} says: Woof!"
 
class Cat(Animal):
    def speak(self):  # Override with different behavior
        return f"{self.name} says: Meow!"
 
# Each class has its own version of speak()
generic_animal = Animal("Generic")
print(generic_animal.speak())  # Output: Generic makes a sound
 
buddy = Dog("Buddy")
print(buddy.speak())  # Output: Buddy says: Woof!
 
whiskers = Cat("Whiskers")
print(whiskers.speak())  # Output: Whiskers says: Meow!

When you call buddy.speak(), Python looks for the speak method in the Dog class first, since buddy is a Dog instance. Because Dog defines its own speak method, Python uses that version. If Dog didn't have a speak method, Python would then look in the parent class Animal and use that version instead.

This search order—starting with the instance's class, then moving to the parent class—is how method overriding works and how subclasses customize inherited behavior.

32.2.2) Why Override Methods?

Method overriding lets you create specialized versions of general behavior. Consider a shape hierarchy:

python
class Shape:
    def __init__(self, name):
        self.name = name
    
    def area(self):
        return 0  # Default implementation
    
    def describe(self):
        return f"{self.name} with area {self.area()}"
 
class Rectangle(Shape):
    def __init__(self, width, height):
        Shape.__init__(self, "Rectangle")
        self.width = width
        self.height = height
    
    def area(self):  # Override with rectangle-specific calculation
        return self.width * self.height
 
class Circle(Shape):
    def __init__(self, radius):
        Shape.__init__(self, "Circle")
        self.radius = radius
    
    def area(self):  # Override with circle-specific calculation
        return 3.14159 * self.radius ** 2
 
# Each shape calculates area differently
rect = Rectangle(5, 3)
print(rect.describe())  # Output: Rectangle with area 15
 
circle = Circle(4)
print(circle.describe())  # Output: Circle with area 50.26544

The describe() method is inherited by both subclasses and works correctly because each subclass provides its own area() implementation.

When you call rect.describe(), the inherited describe() method executes, but self refers to the Rectangle instance. So when describe() calls self.area(), Python looks for area() in the Rectangle class first and finds the overridden version.

32.2.3) Overriding __init__ and Calling Parent Initialization

When you override __init__, you typically need to call the parent's __init__ to ensure the parent's initialization happens:

python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def introduce(self):
        return f"I'm {self.name}, {self.age} years old"
 
class Student(Person):
    def __init__(self, name, age, student_id, major):
        # Call parent's __init__ to set name and age
        Person.__init__(self, name, age)
        # Then set student-specific attributes
        self.student_id = student_id
        self.major = major
    
alice = Student("Alice", 20, "S12345", "Computer Science")
print(alice.name)        # Output: Alice
print(alice.student_id)  # Output: S12345

Notice how Student.__init__ first calls Person.__init__(self, name, age) to initialize the parent class's attributes, then adds its own attributes.

32.3) Using super() to Access Parent Behavior

32.3.1) What super() Does

In the previous section, we called parent methods explicitly: ParentClass.method(self, ...). Python provides a cleaner way: the super() function. super() returns a temporary object that lets you call methods from the parent class without naming the parent explicitly.

python
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"
 
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # Cleaner than Animal.__init__(self, name)
        self.breed = breed
    
    def speak(self):
        parent_sound = super().speak()  # Call parent's speak()
        return f"{parent_sound} - specifically, Woof!"
 
buddy = Dog("Buddy", "Labrador")
print(buddy.speak())
# Output: Buddy makes a sound - specifically, Woof!

Using super() has several advantages:

  • You don't need to name the parent class explicitly
  • It works correctly with multiple inheritance (covered later)
  • It makes code easier to maintain if you change the parent class

32.3.2) Using super() in __init__

The most common use of super() is calling the parent's __init__:

python
class Employee:
    def __init__(self, name, employee_id):
        self.name = name
        self.employee_id = employee_id
        self.is_active = True
    
    def deactivate(self):
        self.is_active = False
 
class Manager(Employee):
    def __init__(self, name, employee_id, department):
        super().__init__(name, employee_id)  # Initialize parent attributes
        self.department = department
        self.team = []  # Manager-specific attribute
    
    def add_team_member(self, employee):
        self.team.append(employee)
 
# Manager gets all Employee attributes plus its own
sarah = Manager("Sarah", "M001", "Engineering")
print(sarah.name)        # Output: Sarah
print(sarah.is_active)   # Output: True
print(sarah.department)  # Output: Engineering

By calling super().__init__(name, employee_id), the Manager class ensures that all the initialization logic from Employee runs, including setting is_active to True.

32.3.3) Extending Parent Methods with super()

You can use super() to extend a parent's method rather than completely replacing it:

python
class BankAccount:
    def __init__(self, account_number, balance):
        self.account_number = account_number
        self.balance = balance
        self.transaction_count = 0
    
    def deposit(self, amount):
        self.balance += amount
        self.transaction_count += 1
        return self.balance
 
class CheckingAccount(BankAccount):
    def __init__(self, account_number, balance, overdraft_limit):
        super().__init__(account_number, balance)
        self.overdraft_limit = overdraft_limit
    
    def deposit(self, amount):
        was_overdrawn = self.balance < 0
        
        # Call parent's deposit to handle the basic logic
        new_balance = super().deposit(amount)
        
        # Add checking-specific behavior
        if was_overdrawn and new_balance >= 0:
            print("Account is no longer overdrawn")
        return new_balance
 
checking = CheckingAccount("C001", -50, 100)
checking.deposit(75)
# Output: Account is no longer overdrawn
print(f"Balance: ${checking.balance}")  # Output: Balance: $25
print(f"Transactions: {checking.transaction_count}")  # Output: Transactions: 1

The CheckingAccount.deposit() method calls super().deposit(amount) to handle the basic deposit logic (updating balance and transaction count), then adds its own check for overdraft status.

32.3.4) When to Use super() vs Direct Parent Call

Use super() in most cases:

python
class Vehicle:
    def __init__(self, brand):
        self.brand = brand
 
class Car(Vehicle):
    def __init__(self, brand, model):
        super().__init__(brand)  # Preferred
        self.model = model

Use direct parent calls when you need to call a specific parent in a multiple inheritance scenario (covered later) or when you want to be explicit about which parent you're calling:

python
class Car(Vehicle):
    def __init__(self, brand, model):
        Vehicle.__init__(self, brand)  # Explicit, but less flexible
        self.model = model

For single inheritance (one parent), super() is almost always the better choice.

32.3.5) super() with Other Methods

You can use super() with any method, not just __init__:

python
class TextProcessor:
    def process(self, text):
        # Basic processing: strip whitespace
        return text.strip()
 
class UppercaseProcessor(TextProcessor):
    def process(self, text):
        # First do parent's processing
        processed = super().process(text)
        # Then add uppercase conversion
        return processed.upper()
 
class PrefixProcessor(UppercaseProcessor):
    def __init__(self, prefix):
        self.prefix = prefix
    
    def process(self, text):
        # First do parent's processing (which calls its parent too)
        processed = super().process(text)
        # Then add prefix
        return f"{self.prefix}: {processed}"
 
processor = PrefixProcessor("ALERT")
result = processor.process("  system error  ")
print(result)  # Output: ALERT: SYSTEM ERROR

32.4) Polymorphism: Working with Compatible Classes

32.4.1) What Polymorphism Means

Polymorphism (from Greek: "many forms") is the ability to treat objects of different classes the same way if they provide the same methods.

In Python, if multiple classes have methods with the same name, you can call those methods without knowing the exact class of the object.

python
class Dog:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} says: Woof!"
 
class Cat:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} says: Meow!"
 
class Bird:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} says: Tweet!"
 
# Function that works with any object that has a speak() method
def make_animal_speak(animal):
    print(animal.speak())
 
# Works with different classes
buddy = Dog("Buddy")
whiskers = Cat("Whiskers")
tweety = Bird("Tweety")
 
make_animal_speak(buddy)     # Output: Buddy says: Woof!
make_animal_speak(whiskers)  # Output: Whiskers says: Meow!
make_animal_speak(tweety)    # Output: Tweety says: Tweet!

The make_animal_speak() function doesn't care what class the animal parameter is—it just needs the object to have a speak() method. This is polymorphism in action.

32.4.2) Polymorphism with Inheritance

Polymorphism is especially powerful with inheritance, where subclasses override parent methods:

python
class PaymentMethod:
    def process_payment(self, amount):
        return f"Processing ${amount:.2f}"
 
class CreditCard(PaymentMethod):
    def __init__(self, card_number):
        self.card_number = card_number
    
    def process_payment(self, amount):
        return f"Charging ${amount:.2f} to credit card ending in {self.card_number[-4:]}"
 
class PayPal(PaymentMethod):
    def __init__(self, email):
        self.email = email
    
    def process_payment(self, amount):
        return f"Sending ${amount:.2f} via PayPal to {self.email}"
 
class BankTransfer(PaymentMethod):
    def __init__(self, account_number):
        self.account_number = account_number
    
    def process_payment(self, amount):
        return f"Transferring ${amount:.2f} to account {self.account_number}"
 
# Function that works with any PaymentMethod
def complete_purchase(payment_method, amount):
    print(payment_method.process_payment(amount))
    print("Purchase complete!")
 
# All payment methods work with the same function
credit = CreditCard("1234567890123456")
paypal = PayPal("user@example.com")
bank = BankTransfer("9876543210")
 
complete_purchase(credit, 99.99)
# Output: Charging $99.99 to credit card ending in 3456
# Output: Purchase complete!
 
complete_purchase(paypal, 49.50)
# Output: Sending $49.50 via PayPal to user@example.com
# Output: Purchase complete!
 
complete_purchase(bank, 199.00)
# Output: Transferring $199.00 to account 9876543210
# Output: Purchase complete!

The complete_purchase() function works with any PaymentMethod subclass. Each subclass provides its own implementation of process_payment(), but the function doesn't need to know which specific class it's working with.

32.4.3) Duck Typing: "If It Walks Like a Duck..."

Python's polymorphism doesn't require classes to be related through inheritance. This is called duck typing: "If it walks like a duck and quacks like a duck, then it is a duck." In other words, Python cares about what an object can do(its methods), not what it is (its class). If an object has the methods you need, you can use it, regardless of its class hierarchy.

python
class FileWriter:
    def __init__(self, filename):
        self.filename = filename
    
    def write(self, data):
        print(f"Writing to {self.filename}: {data}")
 
class DatabaseWriter:
    def __init__(self, table_name):
        self.table_name = table_name
    
    def write(self, data):
        print(f"Inserting into {self.table_name}: {data}")
 
class ConsoleWriter:
    def write(self, data):
        print(f"Console output: {data}")
 
# Function that works with any object that has a write() method
def save_data(writer, data):
    writer.write(data)
 
# All three classes work, even though they're unrelated
file_writer = FileWriter("data.txt")
db_writer = DatabaseWriter("users")
console_writer = ConsoleWriter()
 
save_data(file_writer, "User data")
# Output: Writing to data.txt: User data
 
save_data(db_writer, "User data")
# Output: Inserting into users: User data
 
save_data(console_writer, "User data")
# Output: Console output: User data

None of these classes inherit from a common parent, but they all work with save_data() because they all have a write() method. This is duck typing—the function doesn't care about the class, only about the interface (the methods available).

32.4.4) Practical Example: A Plugin System

Polymorphism enables flexible, extensible systems. Here's a simple plugin system for data processing:

python
class DataProcessor:
    def process(self, data):
        return data  # Base implementation does nothing
 
class UppercaseProcessor(DataProcessor):
    def process(self, data):
        return data.upper()
 
class ReverseProcessor(DataProcessor):
    def process(self, data):
        return data[::-1]
 
class RemoveSpacesProcessor(DataProcessor):
    def process(self, data):
        return data.replace(" ", "")
 
class DataPipeline:
    def __init__(self):
        self.processors = []
    
    def add_processor(self, processor):
        self.processors.append(processor)
    
    def run(self, data):
        result = data
        for processor in self.processors:
            result = processor.process(result)  # Polymorphic call
        return result
 
# Build a processing pipeline
pipeline = DataPipeline()
pipeline.add_processor(UppercaseProcessor())
pipeline.add_processor(RemoveSpacesProcessor())
pipeline.add_processor(ReverseProcessor())
 
# Process data through the pipeline
input_data = "Hello World"
output = pipeline.run(input_data)
print(f"Input:  {input_data}")   # Output: Input:  Hello World
print(f"Output: {output}")        # Output: Output: DLROWOLLEH

The DataPipeline doesn't need to know what specific processors it contains—it just calls process() on each one. You can easily add new processor types without changing the pipeline code.

32.5) Checking Types and Class Relationships (isinstance, issubclass)

32.5.1) Checking Instance Types with isinstance()

Sometimes you need to check if an object is an instance of a particular class. The isinstance() function does this:

python
class Animal:
    pass
 
class Dog(Animal):
    pass
 
class Cat(Animal):
    pass
 
buddy = Dog()
whiskers = Cat()
 
# Check if an object is an instance of a class
print(isinstance(buddy, Dog))     # Output: True
print(isinstance(buddy, Animal))  # Output: True (Dog inherits from Animal)
print(isinstance(buddy, Cat))     # Output: False
 
print(isinstance(whiskers, Cat))    # Output: True
print(isinstance(whiskers, Animal)) # Output: True
print(isinstance(whiskers, Dog))    # Output: False

Notice that isinstance(buddy, Animal) returns True even though buddy is a Dog instance. This is because Dog inherits from Animal, so a Dog instance is also considered an Animal instance.

32.5.2) Why isinstance() Respects Inheritance

The isinstance() function checks the entire inheritance chain:

python
class Vehicle:
    pass
 
class Car(Vehicle):
    pass
 
class ElectricCar(Car):
    pass
 
tesla = ElectricCar()
 
# Check all levels of inheritance
print(isinstance(tesla, ElectricCar))  # Output: True
print(isinstance(tesla, Car))          # Output: True
print(isinstance(tesla, Vehicle))      # Output: True
print(isinstance(tesla, str))          # Output: False

is a

is a

is a

tesla instance

ElectricCar

Car

Vehicle

The tesla object is an instance of ElectricCar, but it's also an instance of Car and Vehicle because of inheritance.

32.5.3) Checking Multiple Types at Once

You can check if an object is an instance of any of several classes by passing a tuple:

python
class Dog:
    pass
 
class Cat:
    pass
 
class Bird:
    pass
 
def is_pet(animal):
    return isinstance(animal, (Dog, Cat, Bird))
 
buddy = Dog()
whiskers = Cat()
tweety = Bird()
rock = "just a rock"
 
print(is_pet(buddy))     # Output: True
print(is_pet(whiskers))  # Output: True
print(is_pet(tweety))    # Output: True
print(is_pet(rock))      # Output: False

This is more concise than writing isinstance(animal, Dog) or isinstance(animal, Cat) or isinstance(animal, Bird).

32.5.4) Checking Class Relationships with issubclass()

The issubclass() function checks if one class is a subclass of another:

python
class Animal:
    pass
 
class Dog(Animal):
    pass
 
class Cat(Animal):
    pass
 
class Poodle(Dog):
    pass
 
# Check class relationships
print(issubclass(Dog, Animal))    # Output: True
print(issubclass(Cat, Animal))    # Output: True
print(issubclass(Poodle, Dog))    # Output: True
print(issubclass(Poodle, Animal)) # Output: True (indirect inheritance)
print(issubclass(Dog, Cat))       # Output: False
 
# A class is considered a subclass of itself
print(issubclass(Dog, Dog))       # Output: True

Note that issubclass() works with classes, not instances. Use isinstance() for instances and issubclass() for classes.

32.5.5) Practical Use Cases for Type Checking

Type checking is useful when you need different behavior for different types:

python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
 
class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus
 
class Contractor:
    def __init__(self, name, hourly_rate, hours):
        self.name = name
        self.hourly_rate = hourly_rate
        self.hours = hours
 
def calculate_payment(worker):
    # When using isinstance() with inheritance, check subclasses before parent classes
    if isinstance(worker, Manager):  # Check Manager first
        return worker.salary + worker.bonus
    elif isinstance(worker, Employee):  # Then check Employee (parent class)
        return worker.salary
    elif isinstance(worker, Contractor):
        return worker.hourly_rate * worker.hours
    else:
        return 0
 
alice = Employee("Alice", 50000)
bob = Manager("Bob", 70000, 10000)
charlie = Contractor("Charlie", 50, 160)
 
print(f"Alice's payment: ${calculate_payment(alice)}")    # Output: Alice's payment: $50000
print(f"Bob's payment: ${calculate_payment(bob)}")        # Output: Bob's payment: $80000
print(f"Charlie's payment: ${calculate_payment(charlie)}")# Output: Charlie's payment: $8000

However, in many cases, polymorphism (having each class implement a common method) is better than type checking:

python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    
    def calculate_payment(self):
        return self.salary
 
class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus
    
    def calculate_payment(self):
        return self.salary + self.bonus
 
class Contractor:
    def __init__(self, name, hourly_rate, hours):
        self.name = name
        self.hourly_rate = hourly_rate
        self.hours = hours
    
    def calculate_payment(self):
        return self.hourly_rate * self.hours
 
# No type checking needed - polymorphism handles it
workers = [
    Employee("Alice", 50000),
    Manager("Bob", 70000, 10000),
    Contractor("Charlie", 50, 160)
]
 
for worker in workers:
    payment = worker.calculate_payment()  # Polymorphic call
    print(f"{worker.name}'s payment: ${payment}")

Output:

Alice's payment: $50000
Bob's payment: $80000
Charlie's payment: $8000

This polymorphic approach is more flexible and easier to extend with new worker types. You don't need to modify the calling code when you add a new worker class—just make sure it has a calculate_payment() method.


Inheritance and polymorphism are powerful tools for organizing code and creating flexible, extensible systems. By creating subclasses, you can reuse existing code while adding or modifying behavior. By overriding methods, you can customize how subclasses work. And by using polymorphism, you can write code that works with many different classes through a common interface.

The key is to use these features thoughtfully:

  • Use inheritance when there's a genuine "is-a" relationship (a Dog is an Animal)
  • Override methods to specialize behavior, not to completely change what a class does
  • Use super() to extend parent behavior rather than replacing it entirely
  • Prefer polymorphism (common methods) over type checking when possible

As you build larger programs, these object-oriented techniques will help you create code that's easier to understand, maintain, and extend.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai