What is Inheritance?

Inheritance lets you create a new class based on an existing one. The new class (child) inherits all attributes and methods from the existing class (parent), and can add or override them.

# Parent class (base class)
class Animal:
    def __init__(self, name, sound):
        self.name = name
        self.sound = sound

    def speak(self):
        print(f"{self.name} says: {self.sound}!")

    def info(self):
        print(f"I'm {self.name}, an animal.")

# Child class (inherits from Animal)
class Dog(Animal):
    def fetch(self, item):
        print(f"{self.name} fetches the {item}!")

# Dog inherits __init__, speak, and info from Animal
rex = Dog("Rex", "Woof")
rex.speak()       # Rex says: Woof!
rex.info()        # I'm Rex, an animal.
rex.fetch("ball") # Rex fetches the ball!

Overriding Methods

Child classes can replace parent methods with their own version:

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

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

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

class Cat(Animal):
    def speak(self):                      # Override parent method
        print(f"{self.name} says: Meow!")

    def info(self):                       # Override parent method
        return f"{self.name} (Cat)"

cat = Cat("Whiskers")
cat.speak()      # Whiskers says: Meow! (uses Cat's version)
print(cat.info())  # Whiskers (Cat)

Using super()

super() calls the parent class's method, letting you extend rather than completely replace it:

class Vehicle:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def info(self):
        return f"{self.year} {self.make} {self.model}"

class ElectricCar(Vehicle):
    def __init__(self, make, model, year, battery_kwh):
        super().__init__(make, model, year)    # Call parent's __init__
        self.battery_kwh = battery_kwh         # Add new attribute

    def info(self):
        base = super().info()                  # Get parent's info
        return f"{base} (Electric, {self.battery_kwh}kWh)"

tesla = ElectricCar("Tesla", "Model 3", 2024, 75)
print(tesla.info())   # 2024 Tesla Model 3 (Electric, 75kWh)
💡
Always use super() in __init__

When your child class has its own __init__, always call super().__init__(...) first to ensure the parent is properly initialized. Forgetting this is a common source of bugs.

Polymorphism

Polymorphism means different classes can be used through the same interface. If multiple classes have the same method name, you can call it without knowing which class the object belongs to:

class Shape:
    def area(self):
        raise NotImplementedError("Subclasses must implement area()")

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        import math
        return math.pi * self.radius ** 2

class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

# Polymorphism in action — same method, different behavior
shapes = [
    Rectangle(10, 5),
    Circle(7),
    Triangle(8, 6)
]

for shape in shapes:
    print(f"{type(shape).__name__}: area = {shape.area():.2f}")

Checking Inheritance

class Animal: pass
class Dog(Animal): pass
class Cat(Animal): pass

rex = Dog()

print(isinstance(rex, Dog))       # True
print(isinstance(rex, Animal))    # True  (Dog inherits from Animal)
print(isinstance(rex, Cat))       # False

print(issubclass(Dog, Animal))    # True
print(issubclass(Cat, Dog))       # False

Composition vs Inheritance

Not everything should use inheritance. Sometimes it's better for a class to contain another class (composition) rather than inherit from it:

# Inheritance: "is a" relationship
# A Dog IS an Animal — inheritance makes sense
class Dog(Animal): pass

# Composition: "has a" relationship
# A Car HAS an Engine — composition makes sense
class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

    def start(self):
        print(f"Engine ({self.horsepower}hp) started!")

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

    def start(self):
        print(f"Starting {self.make}...")
        self.engine.start()

engine = Engine(200)
car = Car("Toyota", engine)
car.start()
# Starting Toyota...
# Engine (200hp) started!
⚠️
Favor composition over deep inheritance

Deep inheritance chains (3+ levels) become hard to understand and maintain. Ask yourself: "Is this an 'is-a' relationship?" If not, use composition.

Practical Example: Notification System

class Notification:
    def __init__(self, recipient, message):
        self.recipient = recipient
        self.message = message

    def send(self):
        raise NotImplementedError

    def __str__(self):
        return f"To: {self.recipient} — {self.message}"


class EmailNotification(Notification):
    def __init__(self, recipient, message, subject):
        super().__init__(recipient, message)
        self.subject = subject

    def send(self):
        print(f"Sending email to {self.recipient}")
        print(f"  Subject: {self.subject}")
        print(f"  Body: {self.message}")


class SMSNotification(Notification):
    def send(self):
        # SMS messages are limited to 160 chars
        msg = self.message[:160]
        print(f"Sending SMS to {self.recipient}: {msg}")


class PushNotification(Notification):
    def __init__(self, recipient, message, app_name):
        super().__init__(recipient, message)
        self.app_name = app_name

    def send(self):
        print(f"Push [{self.app_name}] to {self.recipient}: {self.message}")


# Polymorphism — send all notifications the same way
notifications = [
    EmailNotification("alice@example.com", "Your order shipped!", "Order Update"),
    SMSNotification("+1234567890", "Your code is 482910"),
    PushNotification("alice_device", "New message received", "ChatApp")
]

for notification in notifications:
    notification.send()
    print()

Summary

  • Inheritance creates child classes that inherit from parent classes: class Child(Parent)
  • Override methods by redefining them in the child class
  • super() calls the parent's version of a method
  • Polymorphism lets different classes respond to the same method call differently
  • Use isinstance() and issubclass() to check relationships
  • Prefer composition ("has-a") over inheritance ("is-a") when the relationship isn't clear
🎉
Inheritance and polymorphism mastered!

You now understand the core pillars of OOP in Python. Next up: working with JSON data — a critical skill for APIs, configs, and data exchange.