What Is Inheritance?
Inheritance is the mechanism by which one class (the child or subclass) acquires the properties and behaviour of another class (the parent or superclass). Instead of repeating code, you write it once in the parent and let children extend or specialise it.
Real-world analogy: a Dog and a Cat are both Animals. They share common behaviour (eat, sleep, breathe) defined in Animal, but each has its own specific behaviour (bark vs meow).
class Animal:
"""Parent / Base class."""
def __init__(self, name, species):
self.name = name
self.species = species
def breathe(self):
return f"{self.name} breathes air."
def eat(self, food):
return f"{self.name} eats {food}."
def __repr__(self):
return f"{self.__class__.__name__}(name={self.name!r})"
# βββ Single Inheritance βββββββββββββββββββββββββββββββββββββββ
class Dog(Animal):
"""Child class inheriting from Animal."""
def bark(self):
return f"{self.name} says: Woof!"
class Cat(Animal):
"""Another child class."""
def meow(self):
return f"{self.name} says: Meow!"
dog = Dog("Rex", "Canis lupus familiaris")
cat = Cat("Whiskers", "Felis catus")
# Inherited methods work on child instances
print(dog.breathe()) # Rex breathes air.
print(dog.eat("kibble")) # Rex eats kibble.
print(dog.bark()) # Rex says: Woof!
print(cat.meow()) # Whiskers says: Meow!
Using super() β Calling the Parent
super() returns a proxy object that lets you call methods from the parent class. It is essential in __init__ to initialise the parent's attributes before adding child-specific ones:
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
print(f"Animal.__init__: created {name}")
class Dog(Animal):
def __init__(self, name, breed):
# Call parent's __init__ first
super().__init__(name, "Canis lupus familiaris")
self.breed = breed
print(f"Dog.__init__: breed set to {breed}")
def info(self):
return f"{self.name} is a {self.breed} ({self.species})"
rex = Dog("Rex", "German Shepherd")
print(rex.info())
Forgetting to call super().__init__() means the parent's initialisation code never runs. This leads to missing attributes and mysterious AttributeErrors. Make it a habit.
Method Overriding
A child class can override a parent method by defining a method with the same name. The child version replaces the parent's version for instances of the child class:
class Shape:
def area(self):
raise NotImplementedError("Subclasses must implement area()")
def describe(self):
return f"I am a {self.__class__.__name__} with area {self.area():.2f}"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self): # Overrides Shape.area()
return 3.14159 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self): # Overrides Shape.area()
return self.width * self.height
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side) # Reuse Rectangle.__init__
# Polymorphism: same method name, different behaviour
shapes = [Circle(5), Rectangle(4, 6), Square(3)]
for shape in shapes:
print(shape.describe())
Multilevel Inheritance
A class can inherit from a class that itself inherits from another, creating a chain. Each level can extend or override the behaviour above it:
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start(self):
return f"{self.make} {self.model} starts."
class Car(Vehicle):
def __init__(self, make, model, year, doors):
super().__init__(make, model, year)
self.doors = doors
def honk(self):
return "Beep beep!"
class ElectricCar(Car):
def __init__(self, make, model, year, doors, battery_kwh):
super().__init__(make, model, year, doors)
self.battery_kwh = battery_kwh
def charge_status(self):
return f"Battery: {self.battery_kwh} kWh"
def start(self): # Override Vehicle.start()
return f"{self.make} {self.model} starts silently."
tesla = ElectricCar("Tesla", "Model 3", 2024, 4, 75)
print(tesla.start()) # ElectricCar override
print(tesla.honk()) # From Car
print(tesla.charge_status()) # From ElectricCar
print(f"Year: {tesla.year}") # From Vehicle
Multiple Inheritance
Python allows a class to inherit from more than one parent. This is powerful but must be used carefully to avoid ambiguity:
class Flyable:
def fly(self):
return f"{self.__class__.__name__} is flying!"
def describe(self):
return "I can fly."
class Swimmable:
def swim(self):
return f"{self.__class__.__name__} is swimming!"
def describe(self):
return "I can swim."
class Duck(Flyable, Swimmable):
"""Duck can both fly and swim."""
def quack(self):
return "Quack!"
# Python uses MRO to resolve describe() β Flyable wins (listed first)
# We can be explicit:
def describe(self):
return "I can fly AND swim."
donald = Duck()
print(donald.fly()) # Duck is flying!
print(donald.swim()) # Duck is swimming!
print(donald.quack()) # Quack!
print(donald.describe()) # I can fly AND swim.
# Mixins β the most common use of multiple inheritance
class JSONMixin:
"""Add JSON serialisation to any class."""
import json as _json
def to_json(self):
import json
return json.dumps(self.__dict__, default=str)
class LogMixin:
"""Add simple logging to any class."""
def log(self, msg):
print(f"[{self.__class__.__name__}] {msg}")
class UserService(LogMixin, JSONMixin):
def __init__(self, user_id, email):
self.user_id = user_id
self.email = email
svc = UserService(42, "alice@example.com")
svc.log("User service initialised")
print(svc.to_json())
Method Resolution Order (MRO)
When Python searches for a method in a class hierarchy, it follows a deterministic algorithm called the C3 linearisation. You can inspect the MRO via ClassName.__mro__ or help(ClassName):
class A:
def method(self): return "A"
class B(A):
def method(self): return "B"
class C(A):
def method(self): return "C"
class D(B, C):
pass
# MRO: D β B β C β A β object
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>,
# <class '__main__.A'>, <class 'object'>)
d = D()
print(d.method()) # "B" β first in MRO after D that defines method()
# mro() method returns a list
for cls in D.mro():
print(cls.__name__)
The classic problem with multiple inheritance: if D inherits from B and C which both inherit from A, which A.__init__ gets called? Python's C3 MRO and cooperative super() resolve this cleanly β each class in the MRO is called exactly once when all classes use super().
isinstance() and issubclass()
These two built-in functions let you check type relationships at runtime:
class Animal: pass
class Dog(Animal): pass
class Cat(Animal): pass
rex = Dog()
whiskers = Cat()
# isinstance checks if an object is an instance of a class (or its subclasses)
print(isinstance(rex, Dog)) # True
print(isinstance(rex, Animal)) # True β Dog IS-A Animal
print(isinstance(rex, Cat)) # False
# Check against multiple types using a tuple
print(isinstance(rex, (Dog, Cat))) # True β matches Dog
# issubclass checks class relationships, not instances
print(issubclass(Dog, Animal)) # True
print(issubclass(Cat, Dog)) # False
print(issubclass(Dog, object)) # True β everything inherits from object
# Practical use: type-safe function
def make_sound(animal):
if not isinstance(animal, Animal):
raise TypeError(f"Expected Animal, got {type(animal).__name__}")
if isinstance(animal, Dog):
return "Woof!"
if isinstance(animal, Cat):
return "Meow!"
return "..."
print(make_sound(rex)) # Woof!
print(make_sound(whiskers)) # Meow!
Abstract Base Classes
Abstract classes define a contract: subclasses must implement certain methods. Python enforces this with the abc module:
from abc import ABC, abstractmethod
class PaymentGateway(ABC):
"""Abstract base class β cannot be instantiated directly."""
@abstractmethod
def charge(self, amount: float) -> bool:
"""Charge the customer. Return True on success."""
...
@abstractmethod
def refund(self, amount: float) -> bool:
"""Refund the customer. Return True on success."""
...
# Concrete method shared by all gateways
def process(self, amount: float) -> str:
if amount <= 0:
raise ValueError("Amount must be positive")
success = self.charge(amount)
return f"Payment of ${amount:.2f} {'succeeded' if success else 'failed'}."
class StripeGateway(PaymentGateway):
def charge(self, amount: float) -> bool:
print(f"[Stripe] Charging ${amount:.2f}...")
return True
def refund(self, amount: float) -> bool:
print(f"[Stripe] Refunding ${amount:.2f}...")
return True
class PayPalGateway(PaymentGateway):
def charge(self, amount: float) -> bool:
print(f"[PayPal] Charging ${amount:.2f}...")
return True
def refund(self, amount: float) -> bool:
print(f"[PayPal] Refunding ${amount:.2f}...")
return True
# Cannot instantiate abstract class:
# gateway = PaymentGateway() β TypeError!
stripe = StripeGateway()
print(stripe.process(49.99))
paypal = PayPalGateway()
print(paypal.process(19.99))
| Inheritance Type | Syntax | Use case |
|---|---|---|
| Single | class B(A) | Standard extension / specialisation |
| Multilevel | class C(B) where B extends A | Deep taxonomies, layered frameworks |
| Multiple | class D(A, B) | Mixins, combining orthogonal capabilities |
| Abstract | class E(ABC) with @abstractmethod | Defining interfaces / contracts |
ποΈ Practical Exercise
Design a small OOP hierarchy for a library system:
- Create an abstract class
LibraryItemwith abstract methodget_info(), and concrete attributestitle,item_id. - Create
Book(LibraryItem)that addsauthorandpages. - Create
DVD(LibraryItem)that addsdirectorandruntime_minutes. - Create
EBook(Book)that addsfile_size_mband overridesget_info()to note it is digital. - Write a function that accepts a list of
LibraryItemobjects and prints info for each.
π₯ Challenge Exercise
Implement a plugin system using abstract base classes. Create an abstract DataSource with methods connect(), fetch(query), and disconnect(). Implement two concrete classes: CSVDataSource and JSONDataSource. Write a DataPipeline class that accepts any DataSource, calls its methods, and is completely agnostic of the concrete implementation. Use isinstance to validate input types.
Interview Questions
- What is inheritance and what problem does it solve?
- What is the difference between a base class and a derived class?
- What does
super()do? Why is it preferred over calling the parent class directly? - What is method overriding and how does Python resolve it?
- What is the Method Resolution Order (MRO) in Python?
- What is the diamond problem in multiple inheritance and how does Python solve it?
- What is the difference between
isinstance()andtype()? - What is an abstract class and when would you use one?
- What is the difference between
@abstractmethodand a regular method that raisesNotImplementedError? - What does
issubclass(X, Y)return when X and Y are the same class?
π Summary
- Inheritance allows a child class to reuse attributes and methods from a parent class.
- Single inheritance: one parent. Multilevel: chain of parents. Multiple: more than one parent.
- Always call
super().__init__()in a child's__init__to ensure the parent is properly initialised. - Method overriding lets a child class replace a parent method;
super().method()calls the parent version. - The MRO (C3 linearisation) determines which class's method is found first; inspect it with
ClassName.__mro__. isinstance(obj, Class)returnsTrueifobjis an instance ofClassor any subclass.issubclass(Child, Parent)returnsTrueifChildinherits fromParent.- Abstract base classes (via
abc.ABCand@abstractmethod) enforce that subclasses implement required methods.
Related Topics
Frequently Asked Questions
Yes, Python fully supports multiple inheritance. It is safe when used with care β particularly the Mixin pattern, where classes add narrow, orthogonal capabilities (e.g., LogMixin, JSONMixin) without sharing state. Avoid deep multiple-inheritance hierarchies where classes share attributes and methods, as this creates ambiguity and maintainability issues.
The rule of thumb is: use inheritance when the relationship is "IS-A" (a Dog IS-A Animal), and use composition when it is "HAS-A" (a Car HAS-A Engine). Composition is often more flexible because you can swap out the composed object at runtime, whereas changing a parent class affects the entire hierarchy.
Every class implicitly inherits from object in Python 3. This gives all classes access to methods like __init__, __str__, __repr__, __eq__, and __hash__. You can verify this with MyClass.__mro__ β object always appears at the end.
An abstract base class (interface) defines a contract: it declares what methods must exist but provides no implementation. A mixin provides a concrete implementation of one specific capability that is designed to be mixed into many unrelated classes. In practice, Python uses abstract base classes for type checking and mixins for code reuse through multiple inheritance.