Python Course
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 AnimalRex 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 typeLuna 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()Breed: German Shepherd
super().__init__(...)calls the parent's initialiser — always do this when the child adds new attributessuper()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()Payment of $49.99 processed via CreditCard.
Sending $19.99 via PayPal to alice@example.com.
Payment of $19.99 processed via PayPal.
- Raising
NotImplementedErrorin 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 OrderDonald 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(<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__orClassName.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.