
Python Course
Inheritance in Python
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, super(), multiple inheritance, mixins, and the Method Resolution Order — the complete picture for designing clean, reusable class hierarchies.
Basic Inheritance
To inherit from a class, put the parent class name in parentheses after the child class name. The child automatically receives every attribute and method from the parent.
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 — only exists on 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 only
print(isinstance(rex, Dog)) # True
print(isinstance(rex, Animal)) # True — rex IS an Animal too
print(issubclass(Dog, Animal)) # True — Dog IS a subclass of Animal- The parent class is also called the base class or superclass. The child is the derived class or subclass.
- A child instance passes
isinstance()checks for both its own class and every parent class in its hierarchy. issubclass(Child, Parent)checks the class relationship without needing an instance.- A child class that adds no new code 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.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
def __repr__(self):
return f"{self.__class__.__name__}({self.name!r})"
class Dog(Animal):
def speak(self): # overrides Animal.speak
return f"{self.name} says: Woof!"
class Cat(Animal):
def speak(self): # overrides Animal.speak
return f"{self.name} says: Meow!"
class Fish(Animal):
pass # no override — uses Animal.speak
animals = [Dog("Rex"), Cat("Luna"), Fish("Nemo"), Dog("Buddy")]
for a in animals:
print(a.speak())
# Polymorphism in a real scenario — total sound count
print(f"\n{len(animals)} animals, all speak via the same interface")The super() Function
super() gives access to the parent class from inside a child method. Most commonly used in __init__ to call the parent's initialiser before adding the child's own setup, but works in any method.
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") # run parent __init__ first
self.breed = breed # then add own attributes
def describe(self):
super().describe() # call parent's describe() first
print(f" Breed: {self.breed}")
class GuideDog(Dog):
def __init__(self, name, breed, owner):
super().__init__(name, breed) # runs Dog.__init__ (which runs Animal.__init__)
self.owner = owner
def describe(self):
super().describe() # Dog.describe → Animal.describe → adds breed
print(f" Guide dog for: {self.owner}")
g = GuideDog("Max", "Labrador", "Alice")
g.describe()super().__init__(...)— always call this when the child adds new attributes so the parent's attributes are set up first.super()with no arguments works in Python 3 — no need to pass the class name.- In a chain like
GuideDog → Dog → Animal, eachsuper()call follows the MRO automatically. - Forgetting
super().__init__()means the parent's attributes are never created — a common bug.
Real-World Example — Payment System
class Payment:
def __init__(self, amount, currency="USD"):
self.amount = amount
self.currency = currency
def process(self):
raise NotImplementedError("Subclasses must implement process()")
def receipt(self):
name = self.__class__.__name__
print(f" Receipt: {self.currency} {self.amount:.2f} via {name}")
class CreditCard(Payment):
def __init__(self, amount, last_four, currency="USD"):
super().__init__(amount, currency)
self.last_four = last_four
def process(self):
print(f"Charging {self.currency} {self.amount:.2f} to card ****{self.last_four}")
self.receipt()
class PayPal(Payment):
def __init__(self, amount, email, currency="USD"):
super().__init__(amount, currency)
self.email = email
def process(self):
print(f"PayPal transfer {self.currency} {self.amount:.2f} → {self.email}")
self.receipt()
class BankTransfer(Payment):
def __init__(self, amount, iban, currency="USD"):
super().__init__(amount, currency)
self.iban = iban
def process(self):
print(f"Bank transfer {self.currency} {self.amount:.2f} → {self.iban[-4:].rjust(8,'*')}")
self.receipt()
payments = [
CreditCard(49.99, "4242"),
PayPal(19.99, "alice@example.com"),
BankTransfer(299.00, "GB29NWBK60161331926819", "GBP"),
]
for p in payments:
p.process()
print()- 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.- Adding a new payment method like
ApplePayrequires only writing the new class — nothing else changes.
Mixins — Composable Behaviour
A mixin is a small class that adds one specific capability to other classes without forming a full parent-child hierarchy. Mixins are used with multiple inheritance to compose behaviour like assembling building blocks.
# Mixins — add specific capabilities without full inheritance
class LogMixin:
"""Add log() to any class."""
def log(self, message):
print(f"[{self.__class__.__name__}] {message}")
class SerializeMixin:
"""Add to_dict() to any class."""
def to_dict(self):
return {k: v for k, v in self.__dict__.items() if not k.startswith('_')}
class ValidateMixin:
"""Add validate() — override in subclass for custom rules."""
def validate(self):
raise NotImplementedError
class Product(LogMixin, SerializeMixin, ValidateMixin):
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
def validate(self):
errors = []
if self.price < 0: errors.append("price must be non-negative")
if self.stock < 0: errors.append("stock must be non-negative")
if not self.name: errors.append("name required")
return errors
p = Product("Notebook", 4.99, 50)
p.log("Product created")
print(p.to_dict())
errors = p.validate()
p.log(f"Valid: {not errors}" if not errors else f"Errors: {errors}")- Mixins have no
__init__— they only add methods. - By convention, mixin class names end in
Mixin. - Mixins are the clean alternative to copying the same utility methods into dozens of unrelated classes.
Multiple Inheritance and MRO
Python allows a class to inherit from more than one parent. When method lookup happens, Python follows the Method Resolution Order — a deterministic sequence computed by the C3 linearisation algorithm.
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): # diamond: D → B → C → A → object
pass
d = D()
d.hello() # B is found first in MRO
print(D.__mro__) # full resolution order
print(D.mro()) # same, as a list
# issubclass works across the full hierarchy
print(issubclass(D, A)) # True — A is in D's MRO- Python searches left to right through the MRO and uses the first match it finds.
- Every Python class ultimately inherits from
object— it is always last in the MRO. ClassName.__mro__(tuple) orClassName.mro()(list) show the full resolution order.- The diamond problem — two parents sharing a common grandparent — is resolved automatically by Python's MRO.
Quick Reference 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 version | Redefine same method name in child |
super() | Calls the parent's version of a method | super().__init__(...) |
| Multiple inheritance | Inherit from more than one parent | class C(A, B): |
| Mixin | Small class adding one specific capability | class LogMixin: (no __init__) |
| MRO | Order Python searches classes for methods | ClassName.__mro__ |
issubclass() | Check class relationship without an instance | issubclass(Dog, Animal) |
Practice
What syntax makes a class inherit from a parent class?
What do you call inside a child's __init__ to run the parent's __init__?
What does isinstance(obj, ParentClass) return when obj is an instance of a child class?
What is a mixin in the context of multiple inheritance?
What class does every Python class ultimately inherit from?
Which built-in function checks a class relationship without needing an instance?
Quick Quiz
When a child class defines a method with the same name as a parent method, what happens?
What is the purpose of raising NotImplementedError in a parent class method?
In the MRO for class D(B, C), which class is searched first after D?
What does super() return in Python 3?
Which of the following best describes polymorphism as seen through inheritance?
What is a key characteristic of a mixin class?