indusai.co

Inheritance

Inheritance lets a class reuse and extend another class. The new class (the child) gets every attribute and method of the existing class (the parent) and can add or replace what it needs.

A parent and a child

python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "..."

class Dog(Animal):
    def speak(self):
        return "Woof"

class Cat(Animal):
    def speak(self):
        return "Meow"

pets = [Dog("Rex"), Cat("Tom"), Animal("Generic")]
for p in pets:
    print(p.name, p.speak())

class Dog(Animal) means Dog inherits from Animal. Dog did not define __init__, so Animal's is used. Dog did define speak, so it overrides the parent's version.

Calling the parent with super()

When you override a method but still want the parent's behaviour, call it with super().

python
class Person:
    def __init__(self, name):
        self.name = name

    def describe(self):
        return f"{self.name}"

class Student(Person):
    def __init__(self, name, college):
        super().__init__(name)
        self.college = college

    def describe(self):
        return super().describe() + f", studying at {self.college}"

s = Student("Kiran", "IIT Jodhpur")
print(s.describe())

Forgetting super().__init__() is the most common inheritance bug: the parent's attributes never get set.

isinstance and issubclass

python
class Animal: pass
class Dog(Animal): pass

d = Dog()
print(isinstance(d, Dog))
print(isinstance(d, Animal))
print(issubclass(Dog, Animal))

An instance of a child counts as an instance of the parent.

Method resolution order

When you call a method, Python looks in the object's class first, then its parent, then the grandparent, and so on.

python
class A:
    def who(self): return "A"
class B(A):
    pass
class C(B):
    pass

print(C().who())
print([cls.__name__ for cls in C.__mro__])

Multiple inheritance

A class can have several parents. Python searches them left to right. It works, but keep it simple; mixins with small, focused behaviour are the usual reason to use it.

python
class Walker:
    def move(self): return "walking"
class Swimmer:
    def swim(self): return "swimming"
class Duck(Walker, Swimmer):
    pass

d = Duck()
print(d.move(), d.swim())

Abstract base classes

If a parent method only makes sense when overridden, mark it abstract. Python then refuses to create instances of classes that forget to implement it.

python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        ...

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

print(Square(4).area())
try:
    Shape()
except TypeError as e:
    print("Error:", e)

Composition over inheritance

Inheritance says "a Dog is an Animal". If the relationship is really "a Car has an Engine", store the engine as an attribute instead of inheriting from it. Deep inheritance trees are harder to change than objects that hold other objects.

Practice

  1. Create a Vehicle class with wheels and a describe() method. Make Car and Bike subclasses that set the right number of wheels.
  2. Add an ElectricCar that extends Car with a battery_kwh attribute and overrides describe() using super().
  3. Make an abstract Employee class with an abstract pay() method and two concrete subclasses.