Python inheritance practice super override polymorphism hierarchy programs FP2

Python inheritance practice — 3 real class hierarchies with polymorphism

In the previous article we covered Python inheritance theory. Now it’s time to build real hierarchies. In this article we implement three complete systems — an employee payroll system, a vehicle fleet and a product store — each one using inheritance, super(), method overriding and polymorphism in situations where they genuinely simplify the design.

Python inheritance practice — Program 1: Employee payroll system

This hierarchy models different types of employees with different salary calculation methods. It’s a perfect case for an abstract base class — every employee has a salary, but the way it’s calculated depends on the type.

from abc import ABC, abstractmethod

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

    def __init__(self, employee_id, name, department):
        self.employee_id = employee_id
        self.name = name
        self.department = department
        self._active = True

    @abstractmethod
    def monthly_salary(self):
        """Each subclass must implement its own salary calculation."""
        pass

    @property
    def is_active(self):
        return self._active

    def deactivate(self):
        self._active = False

    def annual_salary(self):
        return self.monthly_salary() * 12

    def payslip(self):
        print(f'\n--- Payslip: {self.name} ({self.employee_id}) ---')
        print(f'Department:      {self.department}')
        print(f'Type:            {self.__class__.__name__}')
        print(f'Monthly salary:  €{self.monthly_salary():.2f}')
        print(f'Annual salary:   €{self.annual_salary():.2f}')
        self._extra_payslip_info()

    def _extra_payslip_info(self):
        """Hook for subclasses to add extra payslip lines."""
        pass

    def __str__(self):
        status = 'Active' if self._active else 'Inactive'
        return (f'[{self.employee_id}] {self.name} '
                f'({self.__class__.__name__}) — '
                f'€{self.monthly_salary():.2f}/month — {status}')

    def __repr__(self):
        return (f"{self.__class__.__name__}(id='{self.employee_id}', "
                f"name='{self.name}')")

    def __lt__(self, other):
        return self.monthly_salary() < other.monthly_salary()

    def __eq__(self, other):
        return isinstance(other, Employee) and self.employee_id == other.employee_id


class FullTimeEmployee(Employee):
    """Fixed monthly salary regardless of hours."""

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

    @property
    def base_salary(self):
        return self._base_salary

    @base_salary.setter
    def base_salary(self, value):
        if value < 0:
            raise ValueError(f'Salary cannot be negative: {value}')
        self._base_salary = round(value, 2)

    def monthly_salary(self):
        return self._base_salary

    def give_raise(self, percentage):
        if not 0 < percentage <= 100:
            raise ValueError(f'Raise must be 1-100%: {percentage}')
        increase = round(self._base_salary * percentage / 100, 2)
        self._base_salary = round(self._base_salary + increase, 2)
        return increase

    def _extra_payslip_info(self):
        print(f'Contract:        Full-time, fixed salary')


class HourlyEmployee(Employee):
    """Paid per hour worked."""

    def __init__(self, employee_id, name, department,
                 hourly_rate, hours_per_month=160):
        super().__init__(employee_id, name, department)
        self._hourly_rate = hourly_rate
        self._hours = hours_per_month

    @property
    def hourly_rate(self):
        return self._hourly_rate

    def log_hours(self, hours):
        if hours < 0:
            raise ValueError(f'Hours cannot be negative: {hours}')
        self._hours = hours

    def monthly_salary(self):
        return round(self._hourly_rate * self._hours, 2)

    def _extra_payslip_info(self):
        print(f'Hours worked:    {self._hours}h × €{self._hourly_rate:.2f}/h')


class CommissionEmployee(Employee):
    """Base salary plus commission on sales."""

    def __init__(self, employee_id, name, department,
                 base_salary, commission_rate):
        super().__init__(employee_id, name, department)
        self._base_salary = base_salary
        self._commission_rate = commission_rate    # percentage
        self._monthly_sales = 0

    def log_sales(self, amount):
        if amount < 0:
            raise ValueError(f'Sales cannot be negative: {amount}')
        self._monthly_sales = round(amount, 2)

    @property
    def commission(self):
        return round(self._monthly_sales * self._commission_rate / 100, 2)

    def monthly_salary(self):
        return round(self._base_salary + self.commission, 2)

    def _extra_payslip_info(self):
        print(f'Base salary:     €{self._base_salary:.2f}')
        print(f'Monthly sales:   €{self._monthly_sales:.2f}')
        print(f'Commission ({self._commission_rate}%):  €{self.commission:.2f}')


class Manager(FullTimeEmployee):
    """Full-time employee with team management bonus."""

    def __init__(self, employee_id, name, department,
                 base_salary, team_size):
        super().__init__(employee_id, name, department, base_salary)
        self._team_size = team_size
        self._team = []

    @property
    def team_size(self):
        return self._team_size

    def add_to_team(self, employee):
        if employee not in self._team:
            self._team.append(employee)
            self._team_size = len(self._team)

    def team_payroll(self):
        return sum(e.monthly_salary() for e in self._team)

    def monthly_salary(self):
        # Base salary + €500 per team member
        bonus = self._team_size * 500
        return round(super().monthly_salary() + bonus, 2)

    def _extra_payslip_info(self):
        super()._extra_payslip_info()
        print(f'Team size:       {self._team_size} employees')
        print(f'Management bonus: €{self._team_size * 500:.2f}')


# Usage
print('=== PAYROLL SYSTEM ===\n')

# Create employees
ceo    = Manager('M001', 'Elena Torres', 'Management', 4000, 0)
sergio = FullTimeEmployee('F001', 'Sergio Medina', 'Engineering', 2500)
maria  = HourlyEmployee('H001', 'María García', 'Support', 15.0, 160)
carlos = CommissionEmployee('C001', 'Carlos Ruiz', 'Sales', 1200, 5)

# Set up manager's team
ceo.add_to_team(sergio)
ceo.add_to_team(maria)
ceo.add_to_team(carlos)

# Log hours and sales
maria.log_hours(172)
carlos.log_sales(45000)

# Print all payslips
all_employees = [ceo, sergio, maria, carlos]
for emp in all_employees:
    emp.payslip()

# Polymorphism — total payroll
print('\n=== PAYROLL SUMMARY ===')
total = sum(e.monthly_salary() for e in all_employees)
print(f'Total monthly payroll: €{total:.2f}')

# Sorting by salary
print('\nRanked by salary:')
for i, emp in enumerate(sorted(all_employees, reverse=True), 1):
    print(f'  {i}. {emp}')

# isinstance checks
print('\nManagers:', [e.name for e in all_employees if isinstance(e, Manager)])
print('Full-time:', [e.name for e in all_employees if isinstance(e, FullTimeEmployee)])

Output:

--- Payslip: Elena Torres (M001) ---
Department:      Management
Type:            Manager
Monthly salary:  €5500.00
Annual salary:   €66000.00
Contract:        Full-time, fixed salary
Team size:       3 employees
Management bonus: €1500.00

--- Payslip: Sergio Medina (F001) ---
...Monthly salary:  €2500.00

--- Payslip: María García (H001) ---
...Hours worked:    172h × €15.00/h
Monthly salary:  €2580.00

--- Payslip: Carlos Ruiz (C001) ---
...Monthly sales:   €45000.00
Commission (5%):  €2250.00
Monthly salary:   €3450.00

=== PAYROLL SUMMARY ===
Total monthly payroll: €14030.00

Ranked by salary:
  1. [M001] Elena Torres (Manager) — €5500.00/month — Active
  2. [C001] Carlos Ruiz (CommissionEmployee) — €3450.00/month — Active
  ...

Managers: ['Elena Torres']
Full-time: ['Elena Torres', 'Sergio Medina']

Notice that Manager inherits from FullTimeEmployee which inherits from Employee — a three-level hierarchy. Manager.monthly_salary() calls super().monthly_salary() (which is FullTimeEmployee‘s version) and adds the team bonus on top. isinstance(ceo, FullTimeEmployee) returns True because Manager is a subclass of FullTimeEmployee. The _extra_payslip_info hook method lets each subclass add its own payslip lines without overriding the entire payslip() method.

Python inheritance practice — Program 2 — Vehicle fleet

This hierarchy models a vehicle fleet with different propulsion types. It introduces the concept of using abstract methods to enforce an interface while sharing common vehicle logic.

from abc import ABC, abstractmethod

class Vehicle(ABC):
    def __init__(self, plate, make, model, year, max_speed):
        self.plate = plate.upper()
        self.make = make
        self.model = model
        self.year = year
        self.max_speed = max_speed
        self._odometer = 0.0
        self._trips = []

    @abstractmethod
    def fuel_type(self):
        pass

    @abstractmethod
    def refuel(self, amount):
        pass

    @abstractmethod
    def range_remaining(self):
        """Returns km remaining with current fuel/charge."""
        pass

    def drive(self, km):
        if km <= 0:
            raise ValueError(f'Distance must be positive: {km}')
        if km > self.range_remaining():
            raise ValueError(
                f'Cannot drive {km}km — only {self.range_remaining():.0f}km remaining'
            )
        self._consume_fuel(km)
        self._odometer = round(self._odometer + km, 1)
        self._trips.append(km)

    @abstractmethod
    def _consume_fuel(self, km):
        """Internal fuel consumption — implemented by each subclass."""
        pass

    @property
    def odometer(self):
        return self._odometer

    @property
    def trips(self):
        return len(self._trips)

    def summary(self):
        print(f'\n{self}')
        print(f'  Fuel type:      {self.fuel_type()}')
        print(f'  Odometer:       {self._odometer}km')
        print(f'  Range left:     {self.range_remaining():.0f}km')
        print(f'  Trips:          {self.trips}')

    def __str__(self):
        return (f'{self.plate} — {self.make} {self.model} {self.year} '
                f'(max {self.max_speed}km/h)')

    def __lt__(self, other):
        return self.max_speed < other.max_speed


class PetrolVehicle(Vehicle):
    CONSUMPTION_PER_100KM = 7.0    # litres

    def __init__(self, plate, make, model, year, max_speed,
                 tank_capacity, initial_fuel=None):
        super().__init__(plate, make, model, year, max_speed)
        self._tank_capacity = tank_capacity
        self._fuel = initial_fuel if initial_fuel is not None else tank_capacity * 0.5

    def fuel_type(self):
        return f'Petrol (tank: {self._tank_capacity}L)'

    def refuel(self, litres):
        if litres <= 0:
            raise ValueError(f'Litres must be positive: {litres}')
        space = self._tank_capacity - self._fuel
        added = min(litres, space)
        self._fuel = round(self._fuel + added, 2)
        print(f'  Refuelled {added:.1f}L → {self._fuel:.1f}L '
              f'({self._fuel/self._tank_capacity*100:.0f}%)')

    def range_remaining(self):
        return round(self._fuel / self.CONSUMPTION_PER_100KM * 100, 1)

    def _consume_fuel(self, km):
        self._fuel = round(
            self._fuel - km * self.CONSUMPTION_PER_100KM / 100, 2
        )


class ElectricVehicle(Vehicle):
    CONSUMPTION_PER_100KM = 15.0    # kWh

    def __init__(self, plate, make, model, year, max_speed,
                 battery_capacity, initial_charge=None):
        super().__init__(plate, make, model, year, max_speed)
        self._battery_capacity = battery_capacity    # kWh
        self._charge = initial_charge if initial_charge is not None \
                       else battery_capacity * 0.8

    def fuel_type(self):
        return f'Electric (battery: {self._battery_capacity}kWh)'

    def refuel(self, kwh):
        """For electric vehicles, refuel means charging."""
        if kwh <= 0:
            raise ValueError(f'kWh must be positive: {kwh}')
        space = self._battery_capacity - self._charge
        added = min(kwh, space)
        self._charge = round(self._charge + added, 2)
        percent = self._charge / self._battery_capacity * 100
        print(f'  Charged {added:.1f}kWh → {self._charge:.1f}kWh ({percent:.0f}%)')

    def range_remaining(self):
        return round(self._charge / self.CONSUMPTION_PER_100KM * 100, 1)

    def _consume_fuel(self, km):
        self._charge = round(
            self._charge - km * self.CONSUMPTION_PER_100KM / 100, 2
        )

    @property
    def charge_percent(self):
        return round(self._charge / self._battery_capacity * 100, 1)


class HybridVehicle(PetrolVehicle, ElectricVehicle):
    """
    Hybrid: uses electric first, switches to petrol when battery low.
    Inherits from both PetrolVehicle and ElectricVehicle.
    """

    ELECTRIC_THRESHOLD = 20.0    # switch to petrol below 20km range

    def __init__(self, plate, make, model, year, max_speed,
                 tank_capacity, battery_capacity):
        PetrolVehicle.__init__(self, plate, make, model, year, max_speed,
                               tank_capacity, tank_capacity * 0.5)
        # Add battery manually (avoid double super().__init__)
        self._battery_capacity = battery_capacity
        self._charge = battery_capacity * 0.8

    def fuel_type(self):
        return (f'Hybrid (petrol: {self._tank_capacity}L + '
                f'electric: {self._battery_capacity}kWh)')

    def range_remaining(self):
        electric_range = self._charge / ElectricVehicle.CONSUMPTION_PER_100KM * 100
        petrol_range = self._fuel / PetrolVehicle.CONSUMPTION_PER_100KM * 100
        return round(electric_range + petrol_range, 1)

    def _consume_fuel(self, km):
        electric_range = self._charge / ElectricVehicle.CONSUMPTION_PER_100KM * 100
        if electric_range > self.ELECTRIC_THRESHOLD:
            kwh_needed = km * ElectricVehicle.CONSUMPTION_PER_100KM / 100
            if kwh_needed <= self._charge:
                self._charge = round(self._charge - kwh_needed, 2)
                return
        # Fall back to petrol
        PetrolVehicle._consume_fuel(self, km)

    def refuel(self, amount, fuel_type='petrol'):
        if fuel_type == 'electric':
            ElectricVehicle.refuel(self, amount)
        else:
            PetrolVehicle.refuel(self, amount)


# Usage
print('=== VEHICLE FLEET ===\n')

yaris   = PetrolVehicle('1234ABC', 'Toyota', 'Yaris', 2022, 170, 45)
tesla   = ElectricVehicle('5678XYZ', 'Tesla', 'Model 3', 2023, 225, 75)
prius   = HybridVehicle('9012DEF', 'Toyota', 'Prius', 2022, 180, 43, 8.8)

fleet = [yaris, tesla, prius]

# Polymorphism — same interface for all vehicle types
for vehicle in fleet:
    vehicle.summary()

# Drive all vehicles the same way
print('\n--- All driving 50km ---')
for vehicle in fleet:
    try:
        vehicle.drive(50)
        print(f'  ✓ {vehicle.plate}: {vehicle.range_remaining():.0f}km remaining')
    except ValueError as err:
        print(f'  ✗ {vehicle.plate}: {err}')

# Refuel — same method, different meaning per type
print('\n--- Refuelling ---')
yaris.refuel(20)              # adds 20 litres of petrol
tesla.refuel(30)              # adds 30 kWh of electricity
prius.refuel(15, 'petrol')    # adds 15 litres of petrol

# Sorting by max speed
print('\nRanked by top speed:')
for i, v in enumerate(sorted(fleet, reverse=True), 1):
    print(f'  {i}. {v}')

# isinstance checks
print('\nElectric or hybrid:',
      [v.plate for v in fleet if isinstance(v, ElectricVehicle)])

Output:

=== VEHICLE FLEET ===

1234ABC — Toyota Yaris 2022 (max 170km/h)
  Fuel type:      Petrol (tank: 45L)
  Odometer:       0km
  Range left:     321km
  Trips:          0

...

--- All driving 50km ---
  ✓ 1234ABC: 286km remaining
  ✓ 5678XYZ: 340km remaining
  ✓ 9012DEF: 409km remaining

--- Refuelling ---
  Refuelled 20.0L → 38.5L (86%)
  Charged 30.0kWh → 62.5kWh (83%)
  Refuelled 15.0L → 36.5L (85%)

Ranked by top speed:
  1. 5678XYZ — Tesla Model 3 2023 (max 225km/h)
  2. 9012DEF — Toyota Prius 2022 (max 180km/h)
  3. 1234ABC — Toyota Yaris 2022 (max 170km/h)

Electric or hybrid: ['5678XYZ', '9012DEF']

Program 3 — Product store with type hierarchy

This hierarchy models a product catalogue where different product types have different pricing rules and stock management.

from abc import ABC, abstractmethod
from datetime import date

class Product(ABC):
    vat_rate = 0.21

    def __init__(self, code, name, base_price):
        self.code = code
        self.name = name
        self._base_price = base_price

    @abstractmethod
    def final_price(self):
        """Price including all adjustments — implemented by each subclass."""
        pass

    @abstractmethod
    def is_available(self):
        pass

    @property
    def base_price(self):
        return self._base_price

    @property
    def price_with_vat(self):
        return round(self.final_price() * (1 + self.vat_rate), 2)

    def __str__(self):
        status = '✓' if self.is_available() else '✗'
        return (f'{status} [{self.code}] {self.name} — '
                f'€{self.final_price():.2f} (€{self.price_with_vat:.2f} VAT)')

    def __lt__(self, other):
        return self.final_price() < other.final_price()

    def __eq__(self, other):
        return isinstance(other, Product) and self.code == other.code


class PhysicalProduct(Product):
    """Product with physical stock."""

    def __init__(self, code, name, base_price, stock=0):
        super().__init__(code, name, base_price)
        self._stock = stock

    @property
    def stock(self):
        return self._stock

    def add_stock(self, quantity):
        if quantity <= 0:
            raise ValueError(f'Quantity must be positive: {quantity}')
        self._stock += quantity

    def sell(self, quantity=1):
        if quantity > self._stock:
            raise ValueError(
                f'Insufficient stock: {self._stock} available, {quantity} requested'
            )
        self._stock -= quantity

    def is_available(self):
        return self._stock > 0

    def final_price(self):
        return round(self._base_price, 2)

    def __str__(self):
        return super().__str__() + f' — stock: {self._stock}'


class DiscountedProduct(PhysicalProduct):
    """Physical product with a percentage discount."""

    def __init__(self, code, name, base_price, stock, discount_pct):
        super().__init__(code, name, base_price, stock)
        self._discount = discount_pct

    @property
    def discount(self):
        return self._discount

    @discount.setter
    def discount(self, value):
        if not 0 <= value <= 100:
            raise ValueError(f'Discount must be 0-100%: {value}')
        self._discount = value

    def final_price(self):
        return round(self._base_price * (1 - self._discount / 100), 2)

    def savings(self):
        return round(self._base_price - self.final_price(), 2)

    def __str__(self):
        return super().__str__() + f' (was €{self._base_price:.2f}, -{self._discount}%)'


class DigitalProduct(Product):
    """Downloadable product — unlimited copies."""

    def __init__(self, code, name, base_price, file_size_mb, format_):
        super().__init__(code, name, base_price)
        self.file_size_mb = file_size_mb
        self.format = format_
        self._downloads = 0

    def download(self):
        self._downloads += 1
        return f'Downloading {self.name} ({self.file_size_mb}MB {self.format})...'

    def is_available(self):
        return True    # digital products never run out

    def final_price(self):
        return round(self._base_price, 2)

    def __str__(self):
        return super().__str__() + f' — {self.format}, {self.file_size_mb}MB'


class SubscriptionProduct(DigitalProduct):
    """Digital product billed monthly."""

    def __init__(self, code, name, monthly_price, features):
        super().__init__(code, name, monthly_price, 0, 'subscription')
        self.features = features
        self._subscribers = 0

    def subscribe(self, customer):
        self._subscribers += 1
        return f'{customer} subscribed to {self.name}'

    def final_price(self):
        return round(self._base_price, 2)    # monthly price

    def annual_price(self):
        return round(self._base_price * 10, 2)    # 2 months free

    def __str__(self):
        base = f'✓ [{self.code}] {self.name} — €{self.final_price():.2f}/month'
        return base + f' ({self._subscribers} subscribers)'


class Store:
    def __init__(self, name):
        self.name = name
        self._catalogue = {}

    def add(self, product):
        self._catalogue[product.code] = product

    def find(self, code):
        if code not in self._catalogue:
            raise ValueError(f'Product not found: {code}')
        return self._catalogue

    def available_products(self):
        return [p for p in self._catalogue.values() if p.is_available()]

    def by_type(self, product_type):
        return [p for p in self._catalogue.values()
                if isinstance(p, product_type)]

    def catalogue(self):
        print(f'\n=== {self.name} Catalogue ===')
        for product in sorted(self._catalogue.values()):
            print(f'  {product}')

    def total_stock_value(self):
        total = 0
        for p in self._catalogue.values():
            if isinstance(p, PhysicalProduct):
                total += p.final_price() * p.stock
        return round(total, 2)


# Usage
print('=== PRODUCT STORE ===\n')

store = Store('Sergio Learns Shop')

# Add products
laptop   = PhysicalProduct('LAP001', 'Laptop Pro', 999.99, 10)
mouse    = DiscountedProduct('MOU001', 'Wireless Mouse', 39.99, 50, 25)
ebook    = DigitalProduct('EBK001', 'Python FP2 Guide', 14.99, 8.5, 'PDF')
premium  = SubscriptionProduct('SUB001', 'Premium Access', 9.99,
                               ['All articles', 'AI tutor', 'No ads'])

for product in [laptop, mouse, ebook, premium]:
    store.add(product)

store.catalogue()

# Polymorphism — same operations for all product types
print('\n--- Available products ---')
for p in store.available_products():
    print(f'  {p.name}: €{p.final_price():.2f}')

# Type-specific operations
print('\n--- Physical products ---')
for p in store.by_type(PhysicalProduct):
    if isinstance(p, DiscountedProduct):
        print(f'  {p.name}: save €{p.savings():.2f} with {p.discount}% discount')
    else:
        print(f'  {p.name}: €{p.final_price():.2f}')

print('\n--- Digital products ---')
for p in store.by_type(DigitalProduct):
    if isinstance(p, SubscriptionProduct):
        print(f'  {p.name}: €{p.final_price():.2f}/month '
              f'or €{p.annual_price():.2f}/year (2 months free)')
    else:
        print(f'  {p.name}: {p.download()}')

# Sales
print('\n--- Sales ---')
try:
    laptop.sell(2)
    print(f'  ✓ Sold 2 laptops → {laptop.stock} remaining')

    print(f'  ✓ {ebook.download()}')
    print(f'  ✓ {premium.subscribe("Sergio")}')

    mouse.sell(100)    # only 50 in stock
except ValueError as err:
    print(f'  ✗ {err}')

print(f'\nTotal stock value: €{store.total_stock_value():.2f}')

Output:

=== PRODUCT STORE ===

=== Sergio Learns Shop Catalogue ===
  ✓ [EBK001] Python FP2 Guide — €14.99 (€18.14 VAT) — PDF, 8.5MB
  ✓ [MOU001] Wireless Mouse — €29.99 (€36.29 VAT) — stock: 50 (was €39.99, -25%)
  ✓ [SUB001] Premium Access — €9.99/month (0 subscribers)
  ✓ [LAP001] Laptop Pro — €999.99 (€1209.99 VAT) — stock: 10

--- Available products ---
  Python FP2 Guide: €14.99
  Wireless Mouse: €29.99
  Premium Access: €9.99
  Laptop Pro: €999.99

--- Physical products ---
  Wireless Mouse: save €10.00 with 25% discount
  Laptop Pro: €999.99

--- Digital products ---
  Python FP2 Guide: Downloading Python FP2 Guide (8.5MB PDF)...
  Premium Access: €9.99/month or €99.90/year (2 months free)

--- Sales ---
  ✓ Sold 2 laptops → 8 remaining
  ✓ Downloading Python FP2 Guide (8.5MB PDF)...
  ✓ Sergio subscribed to Premium Access
  ✗ Insufficient stock: 50 available, 100 requested

Total stock value: €8239.42

Visualise with Python Tutor

Copy this code into pythontutor.com and step through it:

from abc import ABC, abstractmethod

class Shape(ABC):
    def __init__(self, colour):
        self.colour = colour

    @abstractmethod
    def area(self):
        pass

    def describe(self):
        return f'{self.colour}: area={self.area():.2f}'

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

    def area(self):
        return 3.14159 * self.radius ** 2

class Rectangle(Shape):
    def __init__(self, colour, w, h):
        super().__init__(colour)
        self.w = w
        self.h = h

    def area(self):
        return self.w * self.h

shapes = [Circle('red', 5), Rectangle('blue', 4, 6)]
for s in shapes:
    print(s.describe())

Step through and watch three key moments. When Circle('red', 5) is created, Circle.__init__ runs and immediately calls super().__init__(colour) — Python jumps up to Shape.__init__ to set self.colour, then returns to Circle.__init__ to set self.radius. When describe() is called on the Circle, Python looks up describe in Circle — not found, so it goes up to Shape — found. Inside Shape.describe, self.area() is called — but self is a Circle object, so Python calls Circle.area(), not Shape.area(). That's polymorphism — the parent's method calls the child's implementation. Try adding a new shape class and watch it slot into the loop without any changes.

Summary and next step

In this article you practised Python inheritance with three complete hierarchies. You used abstract base classes to enforce interfaces, super() to initialise and extend parent behaviour, method overriding with and without calling super(), isinstance() for type-specific processing, multiple inheritance in the hybrid vehicle, and the hook method pattern for extending behaviour without full override.

In the next article you'll find exercises to solve on your own.

Similar Posts

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *