Object Oriented Programming
Inheritance & super()
Animal ← parent / superclass / base class
Jr Codex Python Notes
Level: Intermediate–Advanced Prerequisites: Chapter 1 Time to complete: ~25 minutes
Table of Contents
- What is Inheritance?
- Creating a Subclass
- Overriding Methods
super()— Calling the Parent's Version- Multi-Level Inheritance
- Multiple Inheritance & the MRO
- 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 AnimalEven 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 soundThis — 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 66. 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 FlyerWhen 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 methodsMRO 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 callsuper().__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
- Why does
Dog(Animal)give everyDoginstance access toAnimal's methods without rewriting them? - What does
super().__init__(name, species="Canine")actually do in theDog.__init__example? - Given
class C(A, B):where bothAandBdefinegreet(), which version doesC().greet()call, and why?
Next Chapter
→ Chapter 3: Polymorphism & Duck Typing
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index