Python Lesson 32 – Inheritance | Dataplexa

Inheritance

One of the most powerful ideas in object-oriented programming is that a new class can be built on top of an existing one — inheriting all of its attributes and methods, and then adding or changing only what needs to be different. This is inheritance, and it is how Python lets you build specialised types without rewriting shared behaviour.

This lesson covers single inheritance, method overriding, the super() function, multiple inheritance, and the method resolution order — the complete picture you need to design clean, reusable class hierarchies.

Basic Inheritance

To inherit from a class, put the parent class name inside parentheses after the child class name. The child class automatically receives every attribute and method from the parent.

Why it exists: real-world entities naturally form hierarchies — an Employee is a kind of Person, a SavingsAccount is a kind of BankAccount. Inheritance lets you model that relationship in code so shared logic lives in one place.

Real-world use: a web framework defines a base View class with request-handling logic. Every specific page — LoginView, DashboardView — inherits from it and only overrides what is unique to that page.

# Basic inheritance — child class inherits parent's methods

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

    def describe(self):
        print(f"{self.name} is a {self.species}.")

    def breathe(self):
        print(f"{self.name} breathes air.")

# Dog inherits everything from Animal
class Dog(Animal):
    def bark(self):            # new method added only to Dog
        print(f"{self.name} says: Woof!")

rex = Dog("Rex", "Canis lupus familiaris")

rex.describe()   # inherited from Animal
rex.breathe()    # inherited from Animal
rex.bark()       # defined on Dog

print(isinstance(rex, Dog))     # True
print(isinstance(rex, Animal))  # True — rex is also an Animal
Rex is a Canis lupus familiaris.
Rex breathes air.
Rex says: Woof!
True
True
  • The parent class is also called the base class or superclass
  • The child class is also called the derived class or subclass
  • A child instance passes isinstance() checks for both its own class and every parent class
  • A child class that adds no code at all can use pass — it still fully inherits the parent

Method Overriding

When a child class defines a method with the same name as a parent method, the child's version overrides the parent's. Python always calls the most specific version — the one closest to the actual object's type.

Why it exists: shared behaviour lives in the parent, but specific behaviour needs to be customised per subclass. Overriding lets each subclass define exactly what a method means for its own type.

# Method overriding — child replaces parent's version

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

    def speak(self):
        print(f"{self.name} makes a sound.")

class Dog(Animal):
    def speak(self):                    # overrides Animal.speak
        print(f"{self.name} says: Woof!")

class Cat(Animal):
    def speak(self):                    # overrides Animal.speak
        print(f"{self.name} says: Meow!")

class Fish(Animal):
    pass                                # no override — uses Animal.speak

animals = [Dog("Rex"), Cat("Luna"), Fish("Nemo")]

for a in animals:
    a.speak()   # Python calls the right version for each type
Rex says: Woof!
Luna says: Meow!
Nemo makes a sound.
  • Python uses the instance's actual type to decide which method to call — this is polymorphism
  • The loop works identically regardless of the specific animal type — each responds to speak() in its own way
  • You can still call the parent's overridden version explicitly using super()

The super() Function

super() gives you access to the parent class from inside a child class method. It is most commonly used in __init__ to call the parent's initialiser before adding the child's own setup, but works in any method.

Why it exists: without super(), you would have to duplicate the parent's __init__ code in every child class. super() calls the parent's version cleanly, keeping the DRY principle intact.

# super() — extending the parent's __init__ and methods

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

    def describe(self):
        print(f"{self.name} ({self.species})")

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, "Canis lupus familiaris")   # call parent __init__
        self.breed = breed                                  # then add own attribute

    def describe(self):
        super().describe()                   # call parent version first
        print(f"Breed: {self.breed}")        # then add extra info

rex = Dog("Rex", "German Shepherd")
rex.describe()
Rex (Canis lupus familiaris)
Breed: German Shepherd
  • super().__init__(...) calls the parent's initialiser — always do this when the child adds new attributes
  • super() with no arguments works in Python 3 — no need to pass the class name
  • You can use super() in any method, not just __init__
  • Forgetting to call super().__init__() means the parent's attributes are never set up — a common bug

A Real-World Inheritance Example

Let's put it all together with a realistic example — a payment system where different payment methods share a common interface but implement the actual charge differently.

# Real-world example — payment method hierarchy

class Payment:
    def __init__(self, amount):
        self.amount = amount

    def process(self):
        raise NotImplementedError("Subclasses must implement process()")

    def receipt(self):
        print(f"Payment of ${self.amount:.2f} processed via {self.__class__.__name__}.")

class CreditCard(Payment):
    def __init__(self, amount, last_four):
        super().__init__(amount)
        self.last_four = last_four

    def process(self):
        print(f"Charging ${self.amount:.2f} to card ending in {self.last_four}.")
        self.receipt()

class PayPal(Payment):
    def __init__(self, amount, email):
        super().__init__(amount)
        self.email = email

    def process(self):
        print(f"Sending ${self.amount:.2f} via PayPal to {self.email}.")
        self.receipt()

payments = [
    CreditCard(49.99, "4242"),
    PayPal(19.99, "alice@example.com")
]

for p in payments:
    p.process()
    print()
Charging $49.99 to card ending in 4242.
Payment of $49.99 processed via CreditCard.

Sending $19.99 via PayPal to alice@example.com.
Payment of $19.99 processed via PayPal.
  • Raising NotImplementedError in the parent's method enforces that every subclass must override it
  • self.__class__.__name__ returns the actual runtime class name — "CreditCard" or "PayPal" — even when called from the parent method
  • This pattern — a shared interface with subclass-specific implementations — is the foundation of polymorphism

Multiple Inheritance

Python allows a class to inherit from more than one parent simultaneously. This is called multiple inheritance. It is powerful but should be used carefully.

# Multiple inheritance — inheriting from two parents

class Flyable:
    def fly(self):
        print(f"{self.name} is flying.")

class Swimmable:
    def swim(self):
        print(f"{self.name} is swimming.")

class Duck(Flyable, Swimmable):
    def __init__(self, name):
        self.name = name

    def quack(self):
        print(f"{self.name} says: Quack!")

donald = Duck("Donald")
donald.fly()     # from Flyable
donald.swim()    # from Swimmable
donald.quack()   # defined on Duck

print(Duck.__mro__)   # Method Resolution Order
Donald is flying.
Donald is swimming.
Donald says: Quack!
(<class 'Duck'>, <class 'Flyable'>, <class 'Swimmable'>, <class 'object'>)
  • List parent classes left to right in the parentheses: class Child(Parent1, Parent2):
  • The most common use of multiple inheritance in Python is mixins — small focused classes that add one specific capability (logging, serialisation, validation)
  • The diamond problem — when two parents share a common grandparent — is resolved automatically by Python's MRO

Method Resolution Order (MRO)

When Python looks up a method on an object, it follows the Method Resolution Order — a specific, deterministic sequence of classes to search. Python uses the C3 linearisation algorithm to compute it.

# MRO — understanding the lookup order

class A:
    def hello(self):
        print("Hello from A")

class B(A):
    def hello(self):
        print("Hello from B")

class C(A):
    def hello(self):
        print("Hello from C")

class D(B, C):   # inherits from both B and C
    pass

d = D()
d.hello()               # which hello() gets called?

print(D.__mro__)        # shows the full resolution order
# D → B → C → A → object
Hello from B
(<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
  • Python searches left to right through the MRO and uses the first match it finds
  • ClassName.__mro__ or ClassName.mro() shows the full resolution order
  • Every class in Python ultimately inherits from object — it is always last in the MRO
  • When designing class hierarchies, keep the MRO in mind to avoid surprising method lookups

Summary Table

Concept What It Does Key Syntax
Inheritance Child receives all parent attributes and methods class Child(Parent):
Method override Child replaces parent's method with its own Redefine same method name in child
super() Calls the parent class version of a method super().__init__(...)
Multiple inheritance Inherit from more than one parent class C(A, B):
MRO Order Python searches classes for methods ClassName.__mro__
Polymorphism Same method name, different behaviour per type Override methods in each subclass

Practice Questions

Practice 1. What syntax is used to make a class inherit from a parent class?



Practice 2. What function do you call inside a child's __init__ to run the parent's __init__?



Practice 3. What does isinstance(obj, ParentClass) return when obj is an instance of a child class?



Practice 4. What is a mixin in the context of multiple inheritance?



Practice 5. What class does every Python class ultimately inherit from?



Quiz

Quiz 1. When a child class defines a method with the same name as a parent method, what happens?






Quiz 2. What is the purpose of raising NotImplementedError in a parent class method?






Quiz 3. In the MRO for class D(B, C), which class is searched first after D?






Quiz 4. What does super() return in Python 3?






Quiz 5. Which of the following best describes polymorphism?






Next up — Polymorphism: one interface, many forms — making different object types respond to the same method call in their own way.