LearnFrenzy
Sign in Take a free test

Object-Oriented Programming · Lesson 32 of 39

Inheritance and Polymorphism

5 min read Updated Nov 7, 2026 Saurabh Samir

Inheritance lets a new class reuse an existing one and add or change only what is different. Polymorphism lets you treat different kinds of objects the same way. Together they keep code short and flexible – the reason every framework, from Django to PyTorch, is built on them.

In this lesson

  • Parent and child classes, overriding methods
  • Calling the parent with super()
  • Polymorphism and duck typing
  • Abstract base classes, and when to prefer composition

A parent and its children

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def annual_pay(self):
        return self.salary * 12

    def describe(self):
        return f"{self.name} earns ₹{self.annual_pay():,} a year"

class Manager(Employee):            # Manager inherits from Employee
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)   # let the parent set name and salary
        self.bonus = bonus

    def annual_pay(self):           # override: same name, new behaviour
        return super().annual_pay() + self.bonus

class Intern(Employee):
    pass                            # inherits everything unchanged

for person in [Employee("Asha", 60000), Manager("Ravi", 90000, 150000), Intern("Kim", 15000)]:
    print(person.describe())

Output

Asha earns ₹720,000 a year
Ravi earns ₹1,230,000 a year
Kim earns ₹180,000 a year

The class family

Employee – name, salary, annual_pay(), describe()

  • Manager(Employee)adds bonus, overrides annual_pay()
  • Intern(Employee)uses everything as it is

Notice that describe() was written once in Employee, yet for Ravi it used Manager's annual_pay(). Python always looks up methods on the actual object's class first.

How Python finds a method

When you call ravi.annual_pay(), Python searches the object's class, then its parent, and so on up to object, the root of every class. This search order is the MRO (method resolution order):

class Employee: pass
class Manager(Employee): pass

print([cls.__name__ for cls in Manager.__mro__])
print(isinstance(Manager(), Employee), issubclass(Manager, Employee))

Output

['Manager', 'Employee', 'object']
True True

super()

super() gives you the parent's version of a method. Use it to extend rather than completely replace behaviour – most often in __init__, so the parent can set up its own attributes.

Common mistake: forgetting super().__init__()

If a child defines its own __init__ and doesn't call super().__init__(...), the parent's attributes are never created. Later code fails with AttributeError: 'Manager' object has no attribute 'salary'.

Polymorphism

"Many forms": the same method call does the right thing for each type. The calling code doesn't need if statements for every kind of object:

class Shape:
    def area(self):
        raise NotImplementedError

class Circle(Shape):
    def __init__(self, r):
        self.r = r
    def area(self):
        return 3.14159 * self.r ** 2

class Square(Shape):
    def __init__(self, side):
        self.side = side
    def area(self):
        return self.side ** 2

shapes = [Circle(1), Square(3), Circle(2)]
for s in shapes:
    print(f"{type(s).__name__:<7}{s.area():8.2f}")
print("Total:", round(sum(s.area() for s in shapes), 2))

Output

Circle     3.14
Square     9.00
Circle    12.57
Total: 24.71

Duck typing

Python doesn't even require a shared parent class. If an object has the method you call, it works – "if it walks like a duck and quacks like a duck, it's a duck":

class EmailAlert:
    def send(self, msg):
        return f"Email: {msg}"

class SmsAlert:
    def send(self, msg):
        return f"SMS: {msg[:20]}"

def notify_all(channels, message):
    for channel in channels:
        print(channel.send(message))

notify_all([EmailAlert(), SmsAlert()], "Your order #4512 has been shipped today")

Output

Email: Your order #4512 has been shipped today
SMS: Your order #4512 has

Abstract base classes

To force every child to implement certain methods, use the abc module. A class with an @abstractmethod cannot be instantiated until a child provides the method:

from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount): ...

class Upi(PaymentMethod):
    def pay(self, amount):
        return f"Paid ₹{amount} via UPI"

print(Upi().pay(250))
try:
    PaymentMethod()
except TypeError as error:
    print("TypeError:", error)

Output

Paid ₹250 via UPI
TypeError: Can't instantiate abstract class PaymentMethod without an implementation for abstract method 'pay'

Composition: "has a" instead of "is a"

Inheritance models an is-a relationship (a Manager is an Employee). When one object merely uses another (a Car has an Engine), store it as an attribute instead. Experienced developers prefer composition whenever inheritance doesn't clearly fit – it keeps classes small and easy to change.

class Engine:
    def __init__(self, hp):
        self.hp = hp
    def start(self):
        return f"{self.hp} hp engine started"

class Car:
    def __init__(self, model, engine):
        self.model = model
        self.engine = engine          # a Car HAS an Engine

    def drive(self):
        return f"{self.model}: {self.engine.start()}"

print(Car("Nexon EV", Engine(143)).drive())

Output

Nexon EV: 143 hp engine started

Practice

Try it yourself

  1. Create Animal with a speak() method and two children, Dog and Cat, that override it. Loop over a list of animals and call speak().

    Show solution
    class Animal:
        def __init__(self, name):
            self.name = name
        def speak(self):
            return "..."
    
    class Dog(Animal):
        def speak(self):
            return f"{self.name} says Woof"
    
    class Cat(Animal):
        def speak(self):
            return f"{self.name} says Meow"
    
    for pet in [Dog("Bruno"), Cat("Misty")]:
        print(pet.speak())

    Output

    Bruno says Woof
    Misty says Meow
  2. Make SavingsAccount inherit from a simple Account(owner, balance) and add an add_interest(rate) method.

    Show solution
    class Account:
        def __init__(self, owner, balance):
            self.owner = owner
            self.balance = balance
    
    class SavingsAccount(Account):
        def add_interest(self, rate):
            self.balance += self.balance * rate / 100
            return self.balance
    
    s = SavingsAccount("Neel", 10000)
    print(s.add_interest(7))

    Output

    10700.0

Quick check

What does super().__init__(...) do?

It runs the parent class's initialiser so the parent's attributes are set up.

What is method overriding?

A child class defines a method with the same name as the parent's, replacing it for objects of the child class.

Is-a or has-a: should Order inherit from Customer?

No. An order has a customer – use composition (self.customer = customer).

Summary

  • class Child(Parent): inherits attributes and methods; override to change behaviour.
  • super() calls the parent's version – always in a child's __init__.
  • Polymorphism and duck typing let one piece of code work with many types.
  • Use ABC to enforce methods; prefer composition for has-a relationships.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Search LearnFrenzy

Esc

Quick links