advanced Step 15 of 20

OOP - Inheritance

Python Programming

OOP - Inheritance

Inheritance is a fundamental OOP concept that allows you to create new classes based on existing ones. The new class (child/subclass) inherits attributes and methods from the existing class (parent/superclass) and can add new functionality or override existing behavior. Inheritance promotes code reuse, establishes clear relationships between types, and enables polymorphism — the ability to treat objects of different classes through a common interface. Python supports single and multiple inheritance with a well-defined method resolution order.

Basic Inheritance

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

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

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

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, "Woof")  # Call parent constructor
        self.breed = breed

    def fetch(self, item):
        return f"{self.name} fetches the {item}!"

class Cat(Animal):
    def __init__(self, name, indoor=True):
        super().__init__(name, "Meow")
        self.indoor = indoor

    def purr(self):
        return f"{self.name} is purring..."

# Usage
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers")

print(dog.speak())    # "Buddy says Woof!" — inherited
print(dog.fetch("ball"))  # "Buddy fetches the ball!" — Dog-specific
print(cat.speak())    # "Whiskers says Meow!" — inherited
print(cat.purr())     # "Whiskers is purring..." — Cat-specific

# isinstance and issubclass
print(isinstance(dog, Dog))      # True
print(isinstance(dog, Animal))   # True
print(issubclass(Dog, Animal))   # True

Method Overriding

class Shape:
    def __init__(self, color="black"):
        self.color = color

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

    def describe(self):
        return f"A {self.color} shape with area {self.area():.2f}"

class Circle(Shape):
    def __init__(self, radius, color="black"):
        super().__init__(color)
        self.radius = radius

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

class Rectangle(Shape):
    def __init__(self, width, height, color="black"):
        super().__init__(color)
        self.width = width
        self.height = height

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

class Square(Rectangle):
    def __init__(self, side, color="black"):
        super().__init__(side, side, color)

# Polymorphism — same interface, different behavior
shapes = [
    Circle(5, "red"),
    Rectangle(4, 6, "blue"),
    Square(3, "green")
]

for shape in shapes:
    print(shape.describe())
# "A red shape with area 78.54"
# "A blue shape with area 24.00"
# "A green shape with area 9.00"

Multiple Inheritance and MRO

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, "Quack")

duck = Duck("Donald")
print(duck.speak())  # "Donald says Quack!"
print(duck.fly())    # "Donald is flying!"
print(duck.swim())   # "Donald is swimming!"

# Method Resolution Order (MRO)
print(Duck.__mro__)
# (, , , , )

# Mixin pattern — small classes that add specific capabilities
class JSONMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__, default=str)

class LogMixin:
    def log(self, message):
        print(f"[{self.__class__.__name__}] {message}")

class User(JSONMixin, LogMixin):
    def __init__(self, name, email):
        self.name = name
        self.email = email

user = User("Alice", "alice@example.com")
print(user.to_json())   # {"name": "Alice", "email": "alice@example.com"}
user.log("User created")  # [User] User created

Abstract Base Classes

from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def connect(self):
        pass

    @abstractmethod
    def execute(self, query):
        pass

    def close(self):
        print("Connection closed")  # Concrete method — can be inherited

class SQLiteDB(Database):
    def connect(self):
        print("Connected to SQLite")
        return self

    def execute(self, query):
        print(f"SQLite executing: {query}")
        return []

class PostgresDB(Database):
    def connect(self):
        print("Connected to PostgreSQL")
        return self

    def execute(self, query):
        print(f"Postgres executing: {query}")
        return []

# db = Database()  # TypeError — cannot instantiate abstract class
db = SQLiteDB()
db.connect()
db.execute("SELECT * FROM users")
db.close()
Pro tip: Favor composition over inheritance when possible. Instead of creating deep class hierarchies, compose objects by including instances of other classes as attributes. Use inheritance for genuine "is-a" relationships and mixins for adding capabilities. Deep inheritance chains become hard to understand and maintain.

Key Takeaways

  • Inheritance lets child classes reuse and extend parent class code; use super() to call parent methods.
  • Method overriding allows subclasses to provide specialized implementations of inherited methods.
  • Polymorphism means different classes can be used interchangeably through a common interface.
  • Python supports multiple inheritance; use the MRO (Class.__mro__) to understand method resolution order.
  • Use Abstract Base Classes (ABC) to define interfaces that subclasses must implement.