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).
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.
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()
super() is like saying, "Do everything grandma did, and then add my extra step on top."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.
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
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
Using super() to Extend, Not Replace
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.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.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 |
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 class with an __init__ method that stores make, model, and year. We also add a fuel_type() method that returns "Gasoline" by default.class Vehicle: with __init__ and fuel_type()class ElectricCar(Vehicle): to make ElectricCar a child of Vehicle. In its __init__, we call super().__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_kwh is newElectricCar, we define fuel_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 all ElectricCar objects.fuel_type() now returns "Electric" for ElectricCar instancesmy_car = ElectricCar("Tesla", "Model 3", 2024, 75). When we call my_car.fuel_type(), Python checks ElectricCar first, finds the overridden method, and returns "Electric". The Vehicle version is never reached.my_car.fuel_type() → "Electric"my_car.make, my_car.model, and my_car.year — all inherited from Vehicle via super(). Plus, my_car.battery_kwh equals 75, which is unique to the child class.battery_kwh = 75 are availableStrengths, 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. |
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. |
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.
Practice Problems
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())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__.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?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.Lesson 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.