What is Inheritance?

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

Basic Inheritance

Create a child class that inherits from a parent:

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"

class Dog(Animal):  # Dog inherits from Animal
    def speak(self):  # Override parent method
        return f"{self.name} barks"

class Cat(Animal):  # Cat inherits from Animal
    def speak(self):  # Override parent method
        return f"{self.name} meows"

# Use the classes
dog = Dog("Buddy")
cat = Cat("Whiskers")

print(dog.speak())  # "Buddy barks"
print(cat.speak())  # "Whiskers meows"

Inheriting Attributes

Child classes inherit parent attributes:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def introduce(self):
        return f"I'm {self.name}, {self.age} years old"

class Student(Person):
    def __init__(self, name, age, student_id):
        super().__init__(name, age)  # Call parent __init__
        self.student_id = student_id
    
    def study(self):
        return f"{self.name} is studying"

student = Student("Alice", 20, "S123")
print(student.introduce())  # Uses inherited method
print(student.study())      # Uses new method

The super() Function

Use super() to call parent class methods:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

class Square(Rectangle):
    def __init__(self, side):
        super().__init__(side, side)  # Call parent __init__
        # Square is a rectangle with equal sides

square = Square(5)
print(square.area())  # 25 (uses inherited method)

Method Overriding

Override parent methods in child class:

class Vehicle:
    def start(self):
        return "Vehicle starting..."

class Car(Vehicle):
    def start(self):  # Override parent method
        return "Car engine starting..."

class ElectricCar(Car):
    def start(self):  # Override again
        return "Electric car silently starting..."

car = Car()
ecar = ElectricCar()

print(car.start())    # "Car engine starting..."
print(ecar.start())   # "Electric car silently starting..."

Multiple Inheritance

Python supports inheriting from multiple parents:

class Flyable:
    def fly(self):
        return "Flying..."

class Swimmable:
    def swim(self):
        return "Swimming..."

class Duck(Flyable, Swimmable):  # Inherits from both
    def quack(self):
        return "Quack!"

duck = Duck()
print(duck.fly())    # "Flying..."
print(duck.swim())   # "Swimming..."
print(duck.quack())  # "Quack!"

Checking Inheritance

Check relationships between classes:

class Animal:
    pass

class Dog(Animal):
    pass

dog = Dog()

# Check if instance is of class
print(isinstance(dog, Dog))      # True
print(isinstance(dog, Animal))   # True

# Check if class is subclass
print(issubclass(Dog, Animal))   # True
print(issubclass(Animal, Dog))   # False

Protected and Private Attributes

Use naming conventions for inheritance:

class Parent:
    def __init__(self):
        self.public = "public"        # Accessible everywhere
        self._protected = "protected" # Convention: should not be accessed outside
        self.__private = "private"    # Name mangling: harder to access

class Child(Parent):
    def show(self):
        print(self.public)      # OK
        print(self._protected)  # OK (but convention says don't)
        # print(self.__private) # Error! Name mangled

Abstract Base Classes

Create classes that must be inherited (using ABC):

from abc import ABC, abstractmethod

class Shape(ABC):  # Abstract base class
    @abstractmethod
    def area(self):
        pass  # Must be implemented by child classes

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):  # Must implement
        return self.width * self.height

# shape = Shape()  # Error! Cannot instantiate abstract class
rect = Rectangle(5, 3)
print(rect.area())  # 15

Common Patterns

Template Method Pattern

class DataProcessor:
    def process(self):
        data = self.load()
        processed = self.transform(data)
        self.save(processed)
    
    def load(self):
        raise NotImplementedError("Subclass must implement")
    
    def transform(self, data):
        return data  # Default implementation
    
    def save(self, data):
        raise NotImplementedError("Subclass must implement")

class CSVProcessor(DataProcessor):
    def load(self):
        return "CSV data"
    
    def save(self, data):
        print(f"Saving {data} to CSV")

Best Practices

  • Use inheritance for "is-a" relationships: Dog is an Animal
  • Use composition for "has-a" relationships: Car has an Engine
  • Don't override unnecessarily: Only override when you need different behavior
  • Use super() consistently: Call parent methods when appropriate
  • Avoid deep inheritance: Keep inheritance hierarchies shallow

Inheritance vs Composition

Sometimes composition (has-a) is better than inheritance (is-a):

# Inheritance (is-a)
class Car(Vehicle):
    pass

# Composition (has-a) - often better
class Car:
    def __init__(self):
        self.engine = Engine()  # Car has an engine
        self.wheels = [Wheel() for _ in range(4)]

Next Steps

Review classes and objects to understand the foundation, or explore dunder methods for advanced class customization.