Inheritance — the basics

How one class can reuse and extend another's behavior — the mechanism the rest of this domain's lessons (composition, Liskov, the patterns) are all reacting to in one way or another.

Beginner

3 min read

A subclass reuses everything from its parent, automatically

class Animal:
    def __init__(self, name):
        self.name = name
 
    def eat(self):
        print(f"{self.name} is eating")
 
class Dog(Animal):          # Dog inherits from Animal
    def bark(self):
        print(f"{self.name} says woof")
 
rex = Dog("Rex")
rex.eat()     # "Rex is eating"  — inherited from Animal
rex.bark()    # "Rex says woof"  — defined directly on Dog

class Dog(Animal): makes Dog a subclass of Animal (the parent or base class) — every method and attribute Animal defines is automatically available on Dog too, without rewriting any of it. Dog didn't need its own __init__ or eat method; it got both for free just by inheriting from Animal, and only had to define what's actually new and specific to dogs (bark).

Overriding: replacing inherited behavior with something more specific

class Animal:
    def make_sound(self):
        print("...")
 
class Dog(Animal):
    def make_sound(self):        # overrides Animal's version
        print("Woof!")
 
class Cat(Animal):
    def make_sound(self):        # overrides Animal's version, differently
        print("Meow!")
 
Dog().make_sound()   # "Woof!"
Cat().make_sound()   # "Meow!"

A subclass can override a method it inherits — define a method with the same name, and the subclass's version replaces the parent's for instances of that subclass. Dog and Cat each override make_sound with their own specific behavior, while still inheriting anything else Animal defines that they don't override.

super(): calling the parent's version instead of replacing it entirely

class Animal:
    def __init__(self, name):
        self.name = name
 
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)     # call Animal's __init__ to set self.name
        self.breed = breed         # then add what's specific to Dog
 
rex = Dog("Rex", "Labrador")
print(rex.name, rex.breed)   # Rex Labrador

Often a subclass wants to extend the parent's behavior rather than fully replace it — Dog still needs name set up exactly the way Animal does it, plus its own extra breed. super().__init__(name) explicitly calls the parent class's __init__, so that logic doesn't need to be duplicated; the subclass adds only what's genuinely new. Without super().__init__(name) here, Dog instances would never actually get a self.name attribute set at all.

Polymorphism: the practical payoff of inheritance

animals = [Dog("Rex"), Cat("Tom")]
 
for animal in animals:
    animal.make_sound()    # calls the right version for each one automatically
Woof!
Meow!

Code that works with Animal objects in general — like this loop — doesn't need to know or check whether each one is actually a Dog or a Cat; calling .make_sound() automatically runs whichever version the actual object's class defines. This is polymorphism: the same code working correctly with any subtype, without needing type-specific branching (if isinstance(animal, Dog): ...). It's the practical reason inheritance is useful beyond just avoiding repeated code — it lets you write code once against a general type and have it correctly handle every specific kind of that type.

Where this gets more nuanced — worth knowing it exists

Inheritance looks simple in small examples like these, but real designs run into real questions: when should a shared behavior be pulled up into a parent class vs. kept separate, what happens when a subclass genuinely can't honor everything its parent promises (covered in the Liskov Substitution lesson), and when composition — building behavior out of separate, swappable pieces instead of a class hierarchy — is actually the better fit (covered in its own lesson, composition over inheritance). This lesson is the mechanical foundation; those lessons are about the judgment calls that come after you're comfortable with the mechanics.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What does a subclass get automatically just by inheriting from a parent class?

2. What does it mean for a subclass to 'override' a method?

3. What problem does super().__init__(...) solve?

4. What does this loop demonstrate: `for animal in animals: animal.make_sound()`?