Python

Object Oriented Programming

Inheritance & super()

Animal ← parent / superclass / base class

JrCodex·6 min read

Jr Codex Python Notes

Level: Intermediate–Advanced Prerequisites: Chapter 1 Time to complete: ~25 minutes


Table of Contents

  1. What is Inheritance?
  2. Creating a Subclass
  3. Overriding Methods
  4. super() — Calling the Parent's Version
  5. Multi-Level Inheritance
  6. Multiple Inheritance & the MRO
  7. Summary & Next Steps

1. What is Inheritance?

Inheritance lets a class (the child/subclass) reuse and extend the attributes and methods of another class (the parent/superclass) — it's how you express "a Dog is a kind of Animal."

Inheritance Relationship
─────────────────────────────────────────
              Animal                    ← parent / superclass / base class
             /      \
          Dog        Cat                 ← children / subclasses
                                          Each Dog IS-AN Animal, each Cat IS-AN Animal
─────────────────────────────────────────

2. Creating a Subclass

class Animal:
    def __init__(self, name):
        self.name = name
 
    def eat(self):
        print(f"{self.name} is eating")
 
    def make_sound(self):
        print(f"{self.name} makes a sound")
 
 
class Dog(Animal):        # Dog INHERITS from Animal
    pass                     # inherits __init__, eat(), make_sound() automatically
 
 
rex = Dog("Rex")
rex.eat()               # "Rex is eating" — inherited from Animal, no code needed in Dog
rex.make_sound()          # "Rex makes a sound" — also inherited
 
print(isinstance(rex, Dog))       # True
print(isinstance(rex, Animal))      # True — a Dog IS ALSO an Animal

Even with an empty body (pass), Dog gets everything Animal defines — that's the entire point of inheritance: reuse without copy-pasting.


3. Overriding Methods

A subclass can override (replace) a parent's method by simply redefining it with the same name:

class Animal:
    def __init__(self, name):
        self.name = name
 
    def make_sound(self):
        print(f"{self.name} makes a generic animal sound")
 
 
class Dog(Animal):
    def make_sound(self):                       # overrides Animal's version
        print(f"{self.name} says Woof!")
 
class Cat(Animal):
    def make_sound(self):                       # a DIFFERENT override
        print(f"{self.name} says Meow!")
 
animals = [Dog("Rex"), Cat("Whiskers"), Animal("Generic")]
for animal in animals:
    animal.make_sound()
# Rex says Woof!
# Whiskers says Meow!
# Generic makes a generic animal sound

This — calling the same method name on different objects and getting behavior specific to each one's actual class — is polymorphism, covered in full in Chapter 3.


4. super() — Calling the Parent's Version

Overriding a method often means "do what the parent does, plus something extra" — not replacing it entirely. super() gives you a reference to the parent class so you can call its version explicitly.

class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species
        print(f"Animal.__init__ called for {name}")
 
    def make_sound(self):
        print(f"{self.name} makes a sound")
 
 
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, species="Canine")   # runs Animal's __init__ first
        self.breed = breed                            # THEN add Dog-specific data
        print(f"Dog.__init__ called for {name}")
 
    def make_sound(self):
        super().make_sound()          # run Animal's version first...
        print(f"{self.name} specifically says Woof!")   # ...then add Dog-specific behavior
 
rex = Dog("Rex", "Labrador")
# Animal.__init__ called for Rex
# Dog.__init__ called for Rex
 
rex.make_sound()
# Rex makes a sound
# Rex specifically says Woof!
 
print(rex.species)      # "Canine" — set by Animal's __init__, via super()
print(rex.breed)           # "Labrador" — set by Dog's own __init__
Without super()                          With super()
─────────────────────────────           ─────────────────────────────
  class Dog(Animal):                       class Dog(Animal):
      def __init__(self, name, breed):         def __init__(self, name, breed):
          self.name = name                         super().__init__(name, species="Canine")
          self.breed = breed                       self.breed = breed
          # species never gets set!                # species IS set, via the parent
          # you'd have to duplicate                # no duplication — parent logic reused
          # Animal's setup logic here
─────────────────────────────           ─────────────────────────────

Rule of thumb: if a subclass defines its own __init__, it should almost always call super().__init__(...) first — otherwise you silently lose whatever setup the parent class was responsible for.


5. Multi-Level Inheritance

Inheritance chains can go more than one level deep — a Puppy can inherit from Dog, which inherits from Animal:

class Animal:
    def __init__(self, name):
        self.name = name
 
    def eat(self):
        print(f"{self.name} is eating")
 
 
class Dog(Animal):
    def make_sound(self):
        print(f"{self.name} says Woof!")
 
 
class Puppy(Dog):
    def play(self):
        print(f"{self.name} is playing energetically!")
 
 
rex_jr = Puppy("Rex Jr.")
rex_jr.eat()          # from Animal (2 levels up)
rex_jr.make_sound()     # from Dog (1 level up)
rex_jr.play()             # from Puppy itself
 
print(Puppy.__mro__)      # shows the full lookup chain, see Section 6

6. Multiple Inheritance & the MRO

Python allows a class to inherit from more than one parent at once:

class Swimmer:
    def swim(self):
        print(f"{self.name} is swimming")
 
class Flyer:
    def fly(self):
        print(f"{self.name} is flying")
 
class Duck(Swimmer, Flyer):        # inherits from BOTH
    def __init__(self, name):
        self.name = name
 
donald = Duck("Donald")
donald.swim()      # "Donald is swimming" — from Swimmer
donald.fly()          # "Donald is flying" — from Flyer

When multiple parents define the same method name, Python needs a deterministic rule for which one wins — this is the Method Resolution Order (MRO):

class A:
    def greet(self):
        print("Hello from A")
 
class B:
    def greet(self):
        print("Hello from B")
 
class C(A, B):        # A is listed FIRST
    pass
 
c = C()
c.greet()               # "Hello from A" — A takes priority because it's listed first
 
print(C.__mro__)
# (<class 'C'>, <class 'A'>, <class 'B'>, <class 'object'>)
# ↑ this tuple shows the exact order Python searches for methods
MRO Rule of Thumb
─────────────────────────────────────────
  class C(A, B):     ← Python checks C, then A, then B, then object
                        (left-to-right, depth-first, avoiding duplicates)
─────────────────────────────────────────

Practical guidance: multiple inheritance is powerful but can get confusing fast if parent classes overlap in what they define. It's most commonly used for mixins — small classes that each add one focused piece of behavior (e.g., a LoggingMixin, a SerializableMixin) rather than full-blown "is-a" hierarchies. Chapter 4 (Composition vs Inheritance) discusses when to reach for a different pattern entirely.


7. Summary & Next Steps

Key Takeaways

  • Inheritance (class Dog(Animal):) lets a subclass reuse a parent's attributes and methods — modeling an "is-a" relationship.
  • A subclass can override a method by redefining it with the same name; super().method(...) calls the parent's version explicitly, typically to extend rather than fully replace it.
  • If a subclass defines its own __init__, it should almost always call super().__init__(...) first to avoid losing the parent's setup logic.
  • Python supports multiple inheritance; when parent classes share a method name, the MRO (ClassName.__mro__) determines which version wins — Python searches left-to-right through the listed parents.

Concept Check

  1. Why does Dog(Animal) give every Dog instance access to Animal's methods without rewriting them?
  2. What does super().__init__(name, species="Canine") actually do in the Dog.__init__ example?
  3. Given class C(A, B): where both A and B define greet(), which version does C().greet() call, and why?

Next Chapter

Chapter 3: Polymorphism & Duck Typing


Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index