12: Single Inheritance

Summary

  • Inheritance is at its most basic about code reuse

  • OOP has a number of different philosophies that attempt to motivate its use.
    • The most common is that classes should model the real world - the domain model

  • The domain model has to be hierarchial because single inheritance creates class hierarchies; the problem is that the world isn’t a simple hierarchy

  • The four pillars of OOP are classes, inheritance, encapsulation, and polymorphism

  • Python allows multiple inheritance, but you can restrict your use of it to single inheritance if you want to

  • Inheritance in Python is a simple extension of the way attributes are resolved
    • First the instance class is created, then the class, then the base class, and then its base class, and so on until the attribute is located

  • Attributes can be overridden by simplify defining them in the class that inherits

  • The super() built-in function can be used to return an attribute from the first class in the inheritance chain to define it

  • The inheritance chain is defined in the class’s __mro__ attribute

  • super() works by searching the __mro__ for the first class to define the method
    • You can specify the class that has the __mro__ to use and the starting class for the search

  • If you restrict yourself to single inheritance you can dispense with super() and access the attribute directly, but with multiple inheritance this simple approach doesn’t work

  • Metaclasses can also be a part of an inheritance chain and there are rules for which metaclass is used.

Program

"""
Inheritance Demonstration Program

This program illustrates key Object-Oriented Programming (OOP) concepts:

- Inheritance as code reuse
- Domain modeling with classes (Vehicle → Car → ElectricCar)
- Encapsulation (attributes inside classes)
- Polymorphism (method overriding)
- Attribute resolution order (__mro__)
- Use of super() for method delegation
- Single inheritance (with notes on multiple inheritance)

The goal is to show how Python resolves attributes and how inheritance chains work.
"""


# -------------------------------
# BASE CLASS (Domain Model Root)
# -------------------------------
class Vehicle:
    """
    A base class representing a general vehicle.
    """

    def __init__(self, brand):
        # Encapsulation: storing data inside the object
        self.brand = brand

    def start(self):
        """
        Basic method that can be reused or overridden.
        """
        return f"{self.brand} vehicle is starting..."

    def info(self):
        """
        General information method.
        """
        return f"This is a vehicle made by {self.brand}"


# -------------------------------
# INTERMEDIATE CLASS
# -------------------------------
class Car(Vehicle):
    """
    Car inherits from Vehicle.
    Demonstrates single inheritance and method extension.
    """

    def __init__(self, brand, doors):
        # Using super() to call parent constructor
        super().__init__(brand)
        self.doors = doors

    def info(self):
        """
        Method overriding (polymorphism).
        Extends parent behavior using super().
        """
        base_info = super().info()  # Calls Vehicle.info()
        return f"{base_info} with {self.doors} doors"

    def start(self):
        """
        Overriding method completely.
        """
        return f"{self.brand} car engine starts with a key."


# -------------------------------
# FINAL CHILD CLASS
# -------------------------------
class ElectricCar(Car):
    """
    ElectricCar inherits from Car.
    Demonstrates deeper inheritance chains.
    """

    def __init__(self, brand, doors, battery_capacity):
        super().__init__(brand, doors)
        self.battery_capacity = battery_capacity

    def start(self):
        """
        Overriding again to demonstrate polymorphism.
        """
        return f"{self.brand} electric car starts silently."

    def battery_info(self):
        return f"Battery capacity: {self.battery_capacity} kWh"


# -------------------------------
# MAIN EXECUTION
# -------------------------------
if __name__ == "__main__":
    # Create an instance of ElectricCar
    tesla = ElectricCar("Tesla", 4, 75)

    # Demonstrate attribute resolution and method calls
    print("START METHOD:")
    print(tesla.start())  # Uses ElectricCar.start()

    print("\nINFO METHOD:")
    print(tesla.info())  # Uses Car.info() → super() → Vehicle.info()

    print("\nBATTERY INFO:")
    print(tesla.battery_info())

    # -------------------------------
    # ATTRIBUTE RESOLUTION ORDER (MRO)
    # -------------------------------
    print("\nMETHOD RESOLUTION ORDER (__mro__):")
    for cls in ElectricCar.__mro__:
        print(cls)

    """
    Output shows how Python searches for attributes:
    1. ElectricCar
    2. Car
    3. Vehicle
    4. object

    This is the inheritance chain used when resolving methods/attributes.
    """

Program Output

START METHOD:
Tesla electric car starts silently.

INFO METHOD:
This is a vehicle made by Tesla with 4 doors

BATTERY INFO:
Battery capacity: 75 kWh

METHOD RESOLUTION ORDER (__mro__):
<class '__main__.ElectricCar'>
<class '__main__.Car'>
<class '__main__.Vehicle'>
<class 'object'>