Inheritance

Inheritance lets a new class build on an existing one: the subclass gets all the attributes and methods of its parent and can add new ones or change how some work. It models "is a" relationships — a SavingsAccount is an Account.

Creating a subclass

PY subclass.py
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} makes a sound"

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

class Dog(Animal):
    def speak(self):
        return f"{self.name} barks"

class Cat(Animal):
    def speak(self):
        return f"{self.name} meows"

for pet in [Dog("Rex"), Cat("Tom"), Animal("Generic")]:
    print(pet.speak(), "|", pet.describe())
Output
Rex barks | I am Rex
Tom meows | I am Tom
Generic makes a sound | I am Generic

class Dog(Animal) makes Dog a subclass. It inherits __init__ and describe, and overrides speak. Code that loops over "animals" can call speak() on any of them and get the right behaviour — this is called polymorphism.

Extending the parent with super()

PY super.py
class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def describe(self):
        return f"{self.owner}: ${self.balance:.2f}"

class SavingsAccount(Account):
    def __init__(self, owner, balance=0, rate=0.02):
        super().__init__(owner, balance)
        self.rate = rate

    def add_interest(self):
        self.balance *= 1 + self.rate

    def describe(self):
        return super().describe() + f" at {self.rate:.0%}"

s = SavingsAccount("Ana", 1000, rate=0.05)
s.add_interest()
print(s.describe())
Output
Ana: $1050.00 at 5%

super() gives access to the parent class's version of a method. Call super().__init__(...) in a subclass initialiser so the parent can set up its own attributes, then add the extra ones.

isinstance and issubclass

PY checks.py
class Shape: ...
class Circle(Shape): ...
class Square(Shape): ...

c = Circle()
print(isinstance(c, Circle), isinstance(c, Shape), isinstance(c, Square))
print(issubclass(Square, Shape), issubclass(Shape, Square))
print(Circle.__mro__)
Output
True True False
True False
(<class '__main__.Circle'>, <class '__main__.Shape'>, <class 'object'>)

Every class ultimately inherits from object. __mro__ ("method resolution order") shows the order Python searches for attributes.

Abstract base classes

An abstract base class defines methods that subclasses must implement. Trying to create an incomplete class fails immediately:

PY abstract.py
from abc import ABC, abstractmethod

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

    def describe(self):
        return f"{type(self).__name__} with area {self.area():.2f}"

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

print(Circle(1).describe())
Square(2)
Output
Circle with area 3.14
Traceback (most recent call last):
  File "main.py", line 22, in <module>
    Square(2)
    ~~~~~~^^^
TypeError: Can't instantiate abstract class Square without an implementation for abstract method 'area'

This catches a forgotten method when the object is created, rather than much later when someone calls it.

Multiple inheritance and mixins

A class can inherit from several parents. The common, safe use is a small mixin that adds one capability:

PY mixins.py
import json

class JsonMixin:
    def to_json(self):
        return json.dumps(vars(self))

class ReprMixin:
    def __repr__(self):
        fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{type(self).__name__}({fields})"

class User(JsonMixin, ReprMixin):
    def __init__(self, name, email):
        self.name = name
        self.email = email

u = User("Ana", "ana@x.com")
print(u)
print(u.to_json())
Output
User(name='Ana', email='ana@x.com')
{"name": "Ana", "email": "ana@x.com"}

Duck typing: inheritance isn't required

Python code usually cares what an object can do, not what it inherits from. Any object with the right method works:

PY duck.py
class Duck:
    def speak(self):
        return "Quack"

class Robot:
    def speak(self):
        return "Beep"

for thing in [Duck(), Robot()]:
    print(thing.speak())
Output
Quack
Beep

"If it walks like a duck and quacks like a duck…" — that's why len() works on any object with a __len__ method.

Composition over inheritance

Inheritance is powerful but couples classes tightly. Often it's simpler for an object to have another object than to be one: a Car has an Engine rather than inheriting from it. Use inheritance for genuine "is a" relationships and shared interfaces; use composition for everything else.

Try it yourself

Try it yourself
Console output