Inheritance

Difficulty: Intermediate

Inheritance allows a class (the child or subclass) to acquire the attributes and methods of another class (the parent or superclass). This promotes code reuse: you define common behavior in a base class and extend or specialize it in subclasses. In Python you specify the parent class in parentheses after the class name, e.g., class Child(Parent).

When a child class defines a method with the same name as one in the parent, it overrides that method. The child's version is called instead of the parent's when invoked on a child instance. If you still need the parent's implementation you call it explicitly using super(). The super() function returns a proxy object that delegates method calls to the parent class, following the method resolution order.

Python supports multiple inheritance, where a class can inherit from more than one parent: class C(A, B). This is powerful but introduces complexity around which parent's method is called when both parents define the same method. Python resolves this using the C3 linearization algorithm, which produces a deterministic Method Resolution Order (MRO). You can inspect the MRO with ClassName.__mro__ or ClassName.mro().

The super() function is MRO-aware. When you call super().method() inside class C, Python does not simply go to C's direct parent. It follows the MRO and calls the next class in the chain. This is essential for cooperative multiple inheritance, where each class in the hierarchy calls super() to ensure every class in the MRO gets a chance to run its version of the method. Without this cooperation, some parent __init__ methods might be skipped.

The isinstance() and issubclass() built-in functions are the idiomatic way to check inheritance relationships at runtime. isinstance(obj, ClassName) returns True if obj is an instance of ClassName or any of its subclasses. issubclass(ChildClass, ParentClass) checks whether one class is derived from another. These are preferred over direct type checks with type() because they respect the inheritance hierarchy.

Code examples

Single inheritance and method overriding

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f'{self.name} makes a sound'

    def info(self):
        return f'Animal: {self.name}'

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # call parent __init__
        self.breed = breed

    def speak(self):  # override parent method
        return f'{self.name} barks'

    def info(self):
        return f'Dog: {self.name}, Breed: {self.breed}'

dog = Dog('Rex', 'Labrador')
print(dog.speak())
print(dog.info())
print(isinstance(dog, Dog))
print(isinstance(dog, Animal))

Dog inherits from Animal and overrides both speak() and info(). super().__init__(name) calls Animal's __init__ so the name attribute is properly set. isinstance confirms that a Dog is also an Animal.

Multiple inheritance and MRO

class A:
    def greet(self):
        return 'Hello from A'

class B(A):
    def greet(self):
        return 'Hello from B'

class C(A):
    def greet(self):
        return 'Hello from C'

class D(B, C):
    pass

d = D()
print(d.greet())
print(D.__mro__)

D inherits from both B and C. Since D does not override greet(), Python follows the MRO: D -> B -> C -> A -> object. B is checked first and has greet(), so B's version runs. The __mro__ tuple shows the full resolution order.

Cooperative multiple inheritance with super()

class Base:
    def __init__(self):
        self.initialized = []
        print('Base init')

class Left(Base):
    def __init__(self):
        super().__init__()
        self.initialized.append('Left')
        print('Left init')

class Right(Base):
    def __init__(self):
        super().__init__()
        self.initialized.append('Right')
        print('Right init')

class Child(Left, Right):
    def __init__(self):
        super().__init__()
        self.initialized.append('Child')
        print('Child init')

c = Child()
print(c.initialized)

Each class calls super().__init__(), and Python follows the MRO: Child -> Left -> Right -> Base. super() in Left does not call Base directly; it calls the next in MRO, which is Right. This ensures every class is initialized exactly once.

isinstance and issubclass checks

class Vehicle:
    pass

class Car(Vehicle):
    pass

class ElectricCar(Car):
    pass

ev = ElectricCar()

print(isinstance(ev, ElectricCar))
print(isinstance(ev, Car))
print(isinstance(ev, Vehicle))
print(issubclass(ElectricCar, Car))
print(issubclass(ElectricCar, Vehicle))
print(issubclass(Car, ElectricCar))

isinstance checks the entire inheritance chain, so an ElectricCar instance is also a Car and a Vehicle. issubclass works on classes, not instances. Car is not a subclass of ElectricCar, so the last check is False.

Key points

Concepts covered

single inheritance, multiple inheritance, super(), MRO, method overriding, isinstance