Inheritance and polymorphism in Python — super(), override and abstract classes
Python Inheritance is the mechanism that lets one class reuse and extend the behaviour of another. If you’ve already built the Student, BankAccount and Product classes from the previous articles, you’ve seen that different classes can share a lot of structure — a Student and a Teacher are both Person objects, a SavingsAccount and a CurrentAccount are both BankAccount objects. Inheritance is how you express and exploit those relationships without repeating code.
This article covers everything: the is-a relationship, super(), method overriding, isinstance(), multiple inheritance and abstract classes with @abstractmethod.
Table of Contents
The is-a relationship — when to use inheritance
Inheritance models the “is-a” relationship. Before creating a subclass ask yourself: “is a [subclass] a [superclass]?” If yes, inheritance makes sense. If not, use composition (one class containing an instance of another).
# IS-A relationships — good candidates for inheritance class Animal: pass class Dog(Animal): pass # A Dog IS AN Animal ✓ class Cat(Animal): pass # A Cat IS AN Animal ✓ class Person: pass class Student(Person): pass # A Student IS A Person ✓ class Teacher(Person): pass # A Teacher IS A Person ✓ # HAS-A relationships — use composition instead class Engine: pass class Car: pass # A Car HAS AN Engine — not IS AN Engine # → car = Car(); car.engine = Engine() ← composition, not inheritance
Basic inheritance syntax
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
def eat(self):
return f'{self.name} is eating'
def __str__(self):
return f'{self.name} ({self.species})'
class Dog(Animal): # Dog inherits from Animal
def __init__(self, name, breed):
super().__init__(name, 'Canis lupus familiaris') # call parent __init__
self.breed = breed
def bark(self): # method unique to Dog
return f'{self.name} barks!'
def __str__(self): # override parent's __str__
return f'{self.name} — {self.breed}'
rex = Dog('Rex', 'German Shepherd')
# Inherited from Animal
print(rex.eat()) # → Rex is eating
print(rex.species) # → Canis lupus familiaris
# Own method
print(rex.bark()) # → Rex barks!
# Overridden __str__
print(rex) # → Rex — German Shepherd (Dog's version, not Animal's)
super() — calling the parent class
super() gives you access to the parent class. Its most common use is in __init__ to initialise the parent’s attributes before adding the subclass’s own:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
return f'I am {self.name}, {self.age} years old'
class Student(Person):
def __init__(self, name, age, student_id, degree):
super().__init__(name, age) # initialise Person first
self.student_id = student_id # then add Student's own attrs
self.degree = degree
self.grades = []
def introduce(self): # override
base = super().introduce() # call Person's introduce()
return f'{base}, studying {self.degree}'
s = Student('Sergio', 20, 'S001', 'GCID')
print(s.name) # → Sergio (from Person)
print(s.degree) # → GCID (from Student)
print(s.introduce()) # → I am Sergio, 20 years old, studying GCID
The golden rule: call super().__init__() as the first line of a subclass __init__. This ensures the parent is fully initialised before you start adding subclass-specific attributes.
Method overriding
A subclass can redefine any method inherited from the parent. The subclass version replaces the parent’s version for that class and all its subclasses:
class Shape:
def __init__(self, colour):
self.colour = colour
def area(self):
return 0 # default — subclasses should override this
def describe(self):
return f'{self.colour} shape with area {self.area():.2f}'
class Circle(Shape):
def __init__(self, colour, radius):
super().__init__(colour)
self.radius = radius
def area(self): # override
import math
return math.pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, colour, width, height):
super().__init__(colour)
self.width = width
self.height = height
def area(self): # override
return self.width * self.height
shapes = [Circle('red', 5), Rectangle('blue', 4, 6)]
for shape in shapes:
print(shape.describe())
# → red shape with area 78.54
# → blue shape with area 24.00
Notice that describe() is defined only in Shape — but when called on a Circle, it uses Circle.area(), not Shape.area(). Python always calls the method from the object’s actual class, not the class where the calling method is defined. This is polymorphism.
Polymorphism — one interface, many implementations
Polymorphism means that different objects can respond to the same method call in different ways. You’ve already seen it above — shape.describe() produces different results for Circle and Rectangle because area() is implemented differently.
class Animal:
def __init__(self, name):
self.name = name
def sound(self):
return '...'
def describe(self):
return f'{self.name} says: {self.sound()}'
class Dog(Animal):
def sound(self): return 'Woof!'
class Cat(Animal):
def sound(self): return 'Meow!'
class Duck(Animal):
def sound(self): return 'Quack!'
animals = [Dog('Rex'), Cat('Whiskers'), Duck('Donald')]
for animal in animals:
print(animal.describe())
# → Rex says: Woof!
# → Whiskers says: Meow!
# → Donald says: Quack!
The loop doesn’t know or care what type each animal is — it just calls describe() and Python dispatches to the right implementation. This is what makes polymorphism powerful: you can add a new animal type (say Lion) and the loop still works without any changes.
isinstance() and issubclass()
isinstance() checks whether an object is an instance of a class or any of its subclasses:
rex = Dog('Rex')
print(isinstance(rex, Dog)) # → True (rex is a Dog)
print(isinstance(rex, Animal)) # → True (Dog inherits from Animal)
print(isinstance(rex, Cat)) # → False
# issubclass() checks class relationships
print(issubclass(Dog, Animal)) # → True
print(issubclass(Cat, Dog)) # → False
print(issubclass(Dog, Dog)) # → True (a class is a subclass of itself)
isinstance() is the right tool when you need to know what type an object is at runtime — for example, to decide how to process it:
def make_sound_loud(animal):
sound = animal.sound()
if isinstance(animal, Dog):
return sound.upper() + '!!'
return sound
print(make_sound_loud(Dog('Rex'))) # → WOOF!!!
print(make_sound_loud(Cat('Mia'))) # → Meow!
Multiple inheritance
Python supports inheriting from more than one parent class:
class Flyable:
def fly(self):
return f'{self.name} is flying'
class Swimmable:
def swim(self):
return f'{self.name} is swimming'
class Duck(Animal, Flyable, Swimmable):
def __init__(self, name):
super().__init__(name)
def sound(self):
return 'Quack!'
donald = Duck('Donald')
print(donald.fly()) # → Donald is flying
print(donald.swim()) # → Donald is swimming
print(donald.sound()) # → Quack!
print(isinstance(donald, Flyable)) # → True
print(isinstance(donald, Swimmable)) # → True
MRO — Method Resolution Order
When a class inherits from multiple parents, Python uses a specific order to decide which parent’s method to call. This order is called MRO (Method Resolution Order) and follows the C3 linearisation algorithm:
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): # inherits from both B and C
pass
d = D()
print(d.method()) # → 'B' — Python finds B first in MRO
print(D.__mro__) # shows the full resolution order
# → (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
The MRO is: D → B → C → A → object. Python searches left to right, depth-first. You can always check the MRO with ClassName.__mro__.
Abstract classes — forcing subclasses to implement methods
An abstract class defines the interface that subclasses must implement. You can’t create instances of an abstract class directly — it’s a blueprint for subclasses.
from abc import ABC, abstractmethod
class Shape(ABC): # ABC = Abstract Base Class
def __init__(self, colour):
self.colour = colour
@abstractmethod
def area(self):
pass # no implementation here
@abstractmethod
def perimeter(self):
pass
def describe(self): # concrete method — not abstract
return (f'{self.colour} shape: '
f'area={self.area():.2f}, '
f'perimeter={self.perimeter():.2f}')
# Cannot instantiate abstract class
s = Shape('red') # → TypeError: Can't instantiate abstract class Shape
# with abstract methods area, perimeter
class Circle(Shape):
def __init__(self, colour, radius):
super().__init__(colour)
self.radius = radius
def area(self): # must implement
import math
return math.pi * self.radius ** 2
def perimeter(self): # must implement
import math
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, colour, width, height):
super().__init__(colour)
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
# Now these work fine
c = Circle('red', 5)
r = Rectangle('blue', 4, 6)
shapes = [c, r]
for shape in shapes:
print(shape.describe())
# → red shape: area=78.54, perimeter=31.42
# → blue shape: area=24.00, perimeter=20.00
If a subclass doesn’t implement all abstract methods, it also becomes abstract and can’t be instantiated:
class IncompleteShape(Shape):
def area(self):
return 42
# Missing perimeter() — still abstract
s = IncompleteShape('green') # → TypeError: Can't instantiate abstract class
A complete inheritance hierarchy
from abc import ABC, abstractmethod
import math
class Animal(ABC):
def __init__(self, name, age):
self.name = name
self.age = age
@abstractmethod
def sound(self):
pass
@abstractmethod
def move(self):
pass
def describe(self):
return (f'{self.name} ({self.__class__.__name__}, {self.age}y): '
f'says "{self.sound()}", {self.move()}')
def __str__(self):
return f'{self.__class__.__name__}({self.name})'
class Domestic(ABC):
def __init__(self, owner):
self.owner = owner
@abstractmethod
def breed(self):
pass
class Dog(Animal, Domestic):
def __init__(self, name, age, owner, breed):
Animal.__init__(self, name, age)
Domestic.__init__(self, owner)
self._breed = breed
def sound(self): return 'Woof!'
def move(self): return 'runs'
def breed(self): return self._breed
def fetch(self): return f'{self.name} fetches the ball!'
class Cat(Animal, Domestic):
def __init__(self, name, age, owner, indoor=True):
Animal.__init__(self, name, age)
Domestic.__init__(self, owner)
self.indoor = indoor
def sound(self): return 'Meow!'
def move(self): return 'sneaks'
def breed(self): return 'Domestic cat'
def purr(self): return f'{self.name} purrs...'
class Wolf(Animal):
def __init__(self, name, age, pack_size):
super().__init__(name, age)
self.pack_size = pack_size
def sound(self): return 'Howl!'
def move(self): return 'hunts'
def howl(self): return f'{self.name} howls at the moon!'
# Usage
rex = Dog('Rex', 3, 'Sergio', 'German Shepherd')
mia = Cat('Mia', 5, 'María')
luna = Wolf('Luna', 4, 8)
animals = [rex, mia, luna]
print('=== ALL ANIMALS ===')
for animal in animals:
print(animal.describe())
print('\n=== DOMESTIC ANIMALS ===')
for animal in animals:
if isinstance(animal, Domestic):
print(f'{animal.name} — owner: {animal.owner}, breed: {animal.breed()}')
print('\n=== SPECIFIC BEHAVIOURS ===')
if isinstance(rex, Dog):
print(rex.fetch())
if isinstance(mia, Cat):
print(mia.purr())
if isinstance(luna, Wolf):
print(luna.howl())
Output:
=== ALL ANIMALS === Rex (Dog, 3y): says "Woof!", runs Mia (Cat, 5y): says "Meow!", sneaks Luna (Wolf, 4y): says "Howl!", hunts === DOMESTIC ANIMALS === Rex — owner: Sergio, breed: German Shepherd Mia — owner: María, breed: Domestic cat === SPECIFIC BEHAVIOURS === Rex fetches the ball! Mia purrs... Luna howls at the moon!
Visualise with Python Tutor
Copy this code into pythontutor.com and step through it:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
return f'I am {self.name}'
class Student(Person):
def __init__(self, name, age, degree):
super().__init__(name, age)
self.degree = degree
def introduce(self):
base = super().introduce()
return f'{base}, studying {self.degree}'
people = [
Person('Carlos', 35),
Student('Sergio', 20, 'GCID')
]
for p in people:
print(p.introduce())
print(isinstance(people[1], Person))
print(isinstance(people[0], Student))
Step through and observe three key moments. When Student.__init__ runs, super().__init__(name, age) calls Person.__init__ — you can see Python temporarily executing code in the parent class’s frame before returning to Student’s. When the for loop calls p.introduce(), Python checks the object’s actual type — for people[0] (a Person) it calls Person.introduce; for people[1] (a Student) it calls Student.introduce. That runtime type dispatch is polymorphism. When Student.introduce calls super().introduce(), it goes up to Person.introduce and gets the base string before adding the degree. Watch how base gets the value 'I am Sergio' from the parent method.
Quick summary
# BASIC INHERITANCE
class Child(Parent):
def __init__(self, child_attr, *parent_args):
super().__init__(*parent_args) # always first
self.child_attr = child_attr
# super() — access parent class
super().__init__(args) # call parent constructor
super().method() # call parent's method version
# METHOD OVERRIDE — redefine in subclass
class Child(Parent):
def method(self): # replaces Parent.method()
parent_result = super().method() # optionally call parent
return parent_result + ' more'
# POLYMORPHISM — same call, different behaviour
for obj in [Dog(), Cat(), Duck()]:
print(obj.sound()) # calls each class's own sound()
# isinstance() — check type at runtime
isinstance(obj, Dog) # True if obj is Dog or subclass of Dog
isinstance(obj, Animal) # True if Dog inherits from Animal
issubclass(Dog, Animal) # True — class relationship, not instance
# MULTIPLE INHERITANCE
class Duck(Animal, Flyable, Swimmable):
def __init__(self, name):
super().__init__(name) # follows MRO
Duck.__mro__ # shows method resolution order
# ABSTRACT CLASSES
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self): # subclasses MUST implement this
pass
def describe(self): # concrete — inherited as-is
return f'Area: {self.area()}'
# Shape() → TypeError — can't instantiate abstract class
# Circle(Shape) with area() → OK
# WHEN TO USE EACH
# Inheritance: A IS A B (Dog is an Animal)
# Composition: A HAS A B (Car has an Engine)
# Abstract class: define interface, enforce implementation in subclasses
# Multiple inheritance: mixin pattern — add capabilities (Flyable, Swimmable)
# self.__class__.__name__ — get the actual class name at runtime
# useful in base class methods that subclasses don't need to override
def __str__(self):
return f'{self.__class__.__name__}({self.name})'
# COMMON ERRORS
# 1. Forgetting super().__init__() → parent attrs not initialised
# 2. Calling super() in wrong order → parent not ready yet
# 3. Overriding without calling super() → lose parent behaviour
# 4. Not implementing all @abstractmethod → TypeError on instantiation
# 5. isinstance vs type: isinstance(dog, Animal) → True
# type(dog) == Animal → False (it's Dog)
In the next article we practice inheritance with real programs — a zoo, an employee hierarchy and a shape system.

2 Comments