SECTION 1
Historical Context & Motivation
As software projects grew larger through the 1960s and 1970s, programmers realized that copying and pasting the same code into dozens of files was a recipe for bugs and confusion. If you needed to change how a single feature worked, you might have to update it in twenty different places. Inheritance was invented as a way for one class to automatically receive the attributes and methods of another, so shared behavior could live in just one place. This single idea became one of the pillars of object-oriented programming (OOP).
1967 • Simula Introduces Inheritance
1972 • Smalltalk Refines OOP
1991 • Python Is Released
2008 • Python 3 Modernizes Classes
object class. This made inheritance behavior more consistent and predictable.The core question that inheritance answers is simple: how do we reuse existing code without duplicating it? And the follow-up question — how do we customize that reused code for specific needs? — is where method overriding enters the picture. Together, these two concepts let you build hierarchies of related classes that share a common foundation while each adding their own twist.
SECTION 2
Core Principles & Definitions
Before writing any code, you need to understand a handful of foundational ideas. These principles explain what inheritance and method overriding are, why they matter, and how they connect to the broader world of object-oriented programming.
Parent (Base) Class
object class.Child (Derived) Class
Inheritance
class Child(Parent):Method Overriding
super()
KEY TAKEAWAY
super() is like saying, "Do everything grandma did, and then add my extra step on top."SECTION 3
Visual Explanation — Class Hierarchy Diagram
The diagram below shows a classic inheritance hierarchy. A parent class called Animal defines shared attributes (name, sound) and a method called speak(). Two child classes — Dog and Cat — inherit from Animal and override speak() to produce their own unique output.
speak(). The Dog and Cat child classes each override speak() (marked with ✦) to produce different output. Both children also add their own unique attributes.Notice the arrows pointing from the parent to the children. In UML (Unified Modeling Language) diagrams used by professional developers, these arrows are drawn the other way — from child to parent — to show that the child "depends on" the parent. For learning purposes, the downward arrows here emphasize that behavior flows from parent to child. Each child automatically receives name and sound without re-declaring them, but each child overrides speak() to customize it.
SECTION 4
How Inheritance & Overriding Work Under the Hood
The Method Resolution Order (MRO)
When you call a method on an object, Python doesn't just look in the object's class. It follows a specific search path called the Method Resolution Order (MRO). Python first checks the object's own class for the method. If it doesn't find the method there, it moves up to the parent class, then the grandparent class, and so on until it either finds the method or raises an AttributeError. This is why overriding works: because Python finds the child's version of a method before it ever reaches the parent's version.
Syntax for Inheritance
INHERITANCE SYNTAX
ChildClass — the new class being created. ParentClass — the existing class placed in parentheses. The child automatically inherits every attribute and method from the parent.
Syntax for Method Overriding
OVERRIDING PATTERN
Define a method inside the child class with exactly the same name as the parent's method. Python will use the child's version whenever this method is called on a child object.
Using super() to Extend, Not Replace
SUPER() PATTERN
super() returns a temporary object of the parent class, allowing you to call the parent's method. This is especially common in __init__ so the child can set up the parent's attributes and then add its own.
Why super() Matters
super(), a child's __init__ would completely replace the parent's __init__, meaning none of the parent's attributes would be created. Using super().__init__() ensures the parent's setup code runs first, and then the child adds its own attributes on top.SECTION 5
Detailed Code Walkthrough
Let's look at a complete, runnable Python example that demonstrates inheritance and method overriding step by step. The diagram below traces what happens when you create a Dog object and call its speak() method.
Dog object and calling speak(). The green dashed line shows super() reaching up to the Animal parent class. Step 5 demonstrates the MRO: Python finds Dog.speak() before it ever checks Animal.speak().Pay close attention to step 2 in the diagram. When Dog.__init__ calls super().__init__(name, "Woof"), it passes "Woof" as the sound argument. This means every Dog automatically gets sound = "Woof" without the caller needing to specify it. The child class simplifies the interface while still relying on the parent to do the heavy lifting.
| Attribute / Method | Defined In | Available On Dog? |
|---|---|---|
name | Animal (parent) | ✅ Yes — inherited |
sound | Animal (parent) | ✅ Yes — inherited |
breed | Dog (child) | ✅ Yes — own attribute |
speak() | Both (overridden in Dog) | ✅ Uses Dog's version |
Summary of attributes and methods available on a Dog object
SECTION 6
Worked Example — Building a Vehicle Hierarchy
Let's build a fresh example from scratch to solidify your understanding. We'll create a Vehicle parent class and an ElectricCar child class that overrides a method.
Vehicle Inheritance Example
- 1
Step 1 — Define the Parent Class
We start by creating aVehicleclass with an__init__method that storesmake,model, andyear. We also add afuel_type()method that returns"Gasoline"by default.class Vehicle:with__init__andfuel_type() - 2
Step 2 — Define the Child Class with Inheritance
We writeclass ElectricCar(Vehicle):to makeElectricCara child ofVehicle. In its__init__, we callsuper().__init__(make, model, year)so the parent's setup runs first. Then we add a new attribute,battery_kwh.super().__init__(make, model, year)inherits parent setup;self.battery_kwhis new - 3
Step 3 — Override the fuel_type() Method
InsideElectricCar, we definefuel_type(self)and return"Electric"instead of"Gasoline". Because the method has the same name as the parent's, Python will use this version for allElectricCarobjects.fuel_type()now returns"Electric"for ElectricCar instances - 4
Step 4 — Create an Object and Test
We create an object:my_car = ElectricCar("Tesla", "Model 3", 2024, 75). When we callmy_car.fuel_type(), Python checksElectricCarfirst, finds the overridden method, and returns"Electric". TheVehicleversion is never reached.my_car.fuel_type()→"Electric" - 5
Step 5 — Verify Inherited Attributes
We can also accessmy_car.make,my_car.model, andmy_car.year— all inherited fromVehicleviasuper(). Plus,my_car.battery_kwhequals75, which is unique to the child class.All parent attributes plus child-specificbattery_kwh = 75are available
SECTION 7
Strengths, Limitations & Common Pitfalls
Inheritance is a powerful tool, but like any tool it can be misused. Understanding both its strengths and its pitfalls will help you decide when to use it and when a different approach might be better.
| Strengths ✅ | Limitations / Pitfalls ⚠️ |
|---|---|
| Code Reuse — Write shared logic once in the parent; all children get it automatically. | Tight Coupling — Changing the parent can break child classes if they depend on specific internal details. |
| Consistency — Related classes share the same interface, making code predictable. | Deep Hierarchies — A chain of five or more parent-child levels becomes confusing and hard to debug. |
| Extensibility — Add new child classes without modifying the parent. | Fragile Base Class — A small change in the parent's method signature can silently break overriding in children. |
| Polymorphism — Different child objects can be used interchangeably wherever the parent type is expected. | Forgetting super() — If you override __init__ without calling super(), parent attributes won't be initialized. |
Strengths vs. limitations of inheritance
DESIGN GUIDELINE
SECTION 8
Connection to Advanced OOP Concepts
The single-inheritance pattern you learned today is just the beginning. As you continue studying Python, you'll encounter more advanced techniques that build directly on these foundations. Here is a quick preview of how today's concepts connect to what comes next.
| What You Know Now | What Comes Next | Key Difference |
|---|---|---|
| Single inheritance (one parent) | Multiple inheritance (two or more parents) | A child can inherit from several classes at once: class C(A, B): |
| Method overriding (replace parent method) | Abstract methods (force child to override) | Using ABC module, the parent can require children to implement a method. |
Using super() in __init__ | MRO with super() in diamond hierarchies | When multiple parents share a grandparent, Python's C3 linearization determines the call order. |
| Inheritance ("is-a" relationship) | Composition ("has-a" relationship) | Instead of inheriting, a class stores other objects as attributes. Often preferred for flexibility. |
How today's concepts connect to advanced OOP topics
Don't worry about mastering all of these right now. The important thing is that single inheritance and method overriding form the foundation for every one of these advanced patterns. Once you feel confident creating parent and child classes, overriding methods, and using super(), you'll be ready to tackle any of these topics.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Show answer
Animal.speak() method might return a generic message, but a Dog needs to return something specific like "Woof!" The child can customize behavior while still inheriting everything else.Question 2: BASIC CALCULATION
class Shape:
def area(self): return 0
class Square(Shape):
def __init__(self, side): self.side = side
def area(self): return self.side * self.side
s = Square(5)
print(s.area())Show answer
25. When s.area() is called, Python checks the Square class first. It finds area() defined there, so it uses self.side * self.side which is 5 × 5 = 25. The parent's area() that returns 0 is never reached because the child's override takes priority.Question 3: INTERMEDIATE
Student class that inherits from a Person class. Person has __init__(self, name, age) and a method greet() that returns "Hi, I'm [name]." The Student class should add a grade attribute and override greet() to return "Hi, I'm [name] and I'm in grade [grade]." Use super() in __init__.Show answer
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
return f"Hi, I'm {self.name}."
class Student(Person):
def __init__(self, name, age, grade):
super().__init__(name, age)
self.grade = grade
def greet(self):
return f"Hi, I'm {self.name} and I'm in grade {self.grade}."
The key decisions are: (1) super().__init__(name, age) lets Person set up name and age; (2) self.grade = grade adds the new attribute; (3) greet() is overridden to include grade information.Question 4: APPLIED
Character class with __init__(self, name, hp) and a method attack(self) that returns 10 (base damage). Create a Mage subclass that adds a mana attribute and overrides attack() so it returns 10 + self.mana // 5 (base damage plus a bonus based on mana, using integer division). If a Mage has 60 mana, what does attack() return?Show answer
Mage.attack() returns 22. The calculation is 10 + 60 // 5 = 10 + 12 = 22. Here is the code:
class Character:
def __init__(self, name, hp): self.name = name; self.hp = hp
def attack(self): return 10
class Mage(Character):
def __init__(self, name, hp, mana): super().__init__(name, hp); self.mana = mana
def attack(self): return 10 + self.mana // 5
The Mage inherits name and hp from Character via super(), adds its own mana, and overrides attack() to factor in magical power.Question 5: CRITICAL THINKING
Penguin class that inherits from a Bird class. The Bird class has a fly() method that returns "Soaring through the sky!" Your classmate overrides fly() in Penguin to return "I can't fly!" Is this a good use of inheritance and overriding? What design problems might this create? Suggest an alternative approach.Show answer
Bird to fly will break logically when it receives a Penguin. This violates the Liskov Substitution Principle, which says a child should be usable anywhere its parent is expected without surprises. A better approach would be to restructure the hierarchy: create a general Bird class without a fly() method, and then create a FlyingBird subclass that adds fly(). Penguin would inherit from Bird (no fly method), while Eagle would inherit from FlyingBird. This avoids overriding a method just to disable it.SUMMARY
Lesson Summary
Summary
Inheritance lets a child class automatically receive all the attributes and methods of a parent class, enabling code reuse and consistent interfaces. You declare it by writing class Child(Parent):. Method overriding allows a child class to replace an inherited method with its own version by defining a method with the same name. Python's Method Resolution Order (MRO) searches the child class first, which is why the override takes priority.
The built-in super() function is essential when you want to extend rather than fully replace a parent's method — especially in __init__ where the parent sets up shared attributes. Always apply the "is-a" test before using inheritance: if the child truly "is a" type of the parent, inheritance is appropriate. With these tools, you can build clean, organized class hierarchies that are easy to read, maintain, and extend.