Python inheritance lets one class reuse the attributes and methods of another. This guide walks through single, multi-level, and hierarchical inheritance with tested examples, plus super(), method overriding, and when inheritance is the wrong tool.
“An algorithm must be seen to be believed.”
Donald Knuth, The Art of Computer Programming
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Intermediate | Reading Time: 14 minutes
Think about a family recipe. Your grandmother has a base recipe for curry. Your mother takes that exact recipe, keeps almost everything, and just adds her own twist. She does not rewrite the whole thing from scratch. That is Python inheritance in one sentence: a new class starts with everything an existing class already has, then changes only the parts it wants to.
Here is the version you will hit at work. You have an Animal class with a name, a speak() method, and an info() method. Now you need Dog, Cat, and Bird. Do you copy and paste that same code three times? No. You write Dog(Animal), and Dog automatically gets everything Animal has, plus whatever Dog-specific behavior you add on top. Write it once, reuse it everywhere.
The original class is the parent (also called the base or superclass). The new class is the child (the derived or subclass). The child inherits every attribute and method from the parent, and it can add its own or override the parent’s. This post covers single inheritance, method overriding, super(), and the practical patterns that make inheritance worth using, plus the mistakes that make it not worth using.
Table of Contents
Single Inheritance: The Foundation
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
This diagram maps the five shapes inheritance can take. Single means one parent and one child. Multi-level is a chain, like grandparent to parent to child. Hierarchical is one parent with several children. Multiple is one child pulling from two or more parents at once. The diamond is the tricky case where two parents share the same ancestor. In real projects, plain single inheritance covers the vast majority of what you write. The diamond is where the Method Resolution Order (MRO) starts to matter, and we save that for the multiple inheritance tutorial. The examples below climb from the simplest pattern to the trickier ones, top to bottom, just like the diagram.
📄 single_inheritance.py: one parent, one child
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
def speak(self):
return f"{self.name} makes a sound"
def info(self):
return f"{self.name} is a {self.species}"
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name, "Dog") # Call parent's __init__
self.breed = breed
def speak(self): # Override parent method
return f"{self.name} says: Woof!"
def fetch(self): # Dog-specific method
return f"{self.name} fetches the ball!"
buddy = Dog("Buddy", "Labrador")
print(buddy.speak()) # Dog's version
print(buddy.info()) # Inherited from Animal
print(buddy.fetch()) # Dog-only method
print(isinstance(buddy, Dog)) # True
print(isinstance(buddy, Animal)) # True, a Dog IS an Animal
▶ Output
Buddy says: Woof! Buddy is a Dog Buddy fetches the ball! True True
What happened here: The line class Dog(Animal) is the whole trick. Putting Animal in those parentheses tells Python that Dog is a kind of Animal, so Dog starts with everything Animal has. Inside Dog.__init__, the call super().__init__(name, "Dog") runs the parent constructor, so we do not rewrite the code that sets name and species. We define speak() again in Dog, which overrides (replaces) the Animal version, so calling buddy.speak() runs the Dog one. We never defined info() in Dog, so Python falls back to Animal’s version for free. And both isinstance checks print True, because through inheritance a Dog genuinely IS an Animal.
super(): Calling the Parent
Overriding a method fully replaces the parent version. But often you do not want to throw the parent away, you just want to add to it. That is what super() is for. It hands you the parent so you can run its method, then build on the result. Think of forwarding an email: you keep the original message and add your own note on top instead of retyping the whole thing.
📄 super_usage.py: extend behavior instead of replacing it
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def describe(self):
return f"{self.year} {self.make} {self.model}"
class ElectricCar(Vehicle):
def __init__(self, make, model, year, battery_kwh):
super().__init__(make, model, year) # Reuse parent init
self.battery_kwh = battery_kwh
self.charge_level = 100
def describe(self):
base = super().describe() # Reuse parent's describe
return f"{base} (Electric, {self.battery_kwh}kWh, {self.charge_level}% charged)"
def drive(self, km):
drain = km * 0.2 # 0.2% per km
self.charge_level = max(0, self.charge_level - drain)
return f"Drove {km}km. Charge: {self.charge_level:.0f}%"
tesla = ElectricCar("Tesla", "Model 3", 2024, 75)
print(tesla.describe())
print(tesla.drive(100))
print(tesla.describe())
▶ Output
2024 Tesla Model 3 (Electric, 75kWh, 100% charged) Drove 100km. Charge: 80% 2024 Tesla Model 3 (Electric, 75kWh, 80.0% charged)
What happened here: super() gives you a handle to the parent class. In __init__, super().__init__(make, model, year) sets up the parent’s attributes so the child does not repeat that work. In describe(), the child calls super().describe() to get the parent’s string, then tacks the electric details on the end. That is the “extend, do not replace” pattern: build on top of the parent instead of rewriting it.
One small detail worth catching: the last line prints 80.0%, not 80%. Look closely at the two methods. drive() formats its number with {self.charge_level:.0f}, which rounds to a whole number for display, so it shows 80%. But describe() just drops the value in with plain {self.charge_level}, and by then charge_level is the float 80.0 (because 100 - 100*0.2 produces a float). Same value, two different format strings, two different looking outputs. If you want a clean 80% there too, format it with :.0f in describe() as well.
Multi-level Inheritance
Python inheritance can go more than one level deep. A child can have a parent, and that parent can have its own parent. Picture three generations: grandparent, parent, child. Each generation passes its features down the line, and the youngest one ends up with all of them.
📄 multilevel.py: grandparent to parent to child
class Animal:
def __init__(self, name):
self.name = name
def breathe(self):
return f"{self.name} is breathing"
class Mammal(Animal):
def __init__(self, name, fur_color):
super().__init__(name)
self.fur_color = fur_color
def feed_young(self):
return f"{self.name} feeds young with milk"
class Dog(Mammal):
def __init__(self, name, fur_color, breed):
super().__init__(name, fur_color)
self.breed = breed
def bark(self):
return f"{self.name} the {self.breed} barks!"
rex = Dog("Rex", "brown", "German Shepherd")
print(rex.bark()) # Dog method
print(rex.feed_young()) # Mammal method
print(rex.breathe()) # Animal method
print(f"MRO: {[c.__name__ for c in Dog.__mro__]}")
▶ Output
Rex the German Shepherd barks! Rex feeds young with milk Rex is breathing MRO: ['Dog', 'Mammal', 'Animal', 'object']
What happened here: Dog inherits from Mammal, and Mammal inherits from Animal. So rex can call methods from all three levels: bark() from Dog, feed_young() from Mammal, and breathe() from Animal. When you call a method, Python searches the classes in a fixed order called the MRO: Dog first, then Mammal, then Animal, then the built-in object that every class inherits from. The last line prints that exact order. Notice the chain of super().__init__() calls too: Dog calls Mammal’s init, which calls Animal’s init, so every level gets a chance to set up its own attributes. Skip one link and that level’s attributes silently never get set, which is the exact trap we hit in the catch section below.
Hierarchical Inheritance
Flip the multi-level idea sideways and you get hierarchical inheritance: one parent with several children, like siblings sharing the same parents. Every child gets the shared family traits, but each one does its own thing. A Circle, a Rectangle, and a Triangle are all shapes, they all have a color, but each computes its area differently.
📄 hierarchical.py: one parent, multiple children
class Shape:
def __init__(self, color="black"):
self.color = color
def describe(self):
return f"A {self.color} {self.__class__.__name__}"
class Circle(Shape):
def __init__(self, radius, color="black"):
super().__init__(color)
self.radius = radius
def area(self):
from math import pi
return pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height, color="black"):
super().__init__(color)
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Triangle(Shape):
def __init__(self, base, height, color="black"):
super().__init__(color)
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
shapes = [
Circle(5, "red"),
Rectangle(4, 6, "blue"),
Triangle(8, 3, "green"),
]
for s in shapes:
print(f"{s.describe()}: area = {s.area():.2f}")
▶ Output
A red Circle: area = 78.54 A blue Rectangle: area = 24.00 A green Triangle: area = 12.00
What happened here: Three classes inherit from one parent. Circle, Rectangle, and Triangle all get color and describe() from Shape for free, but each writes its own area(). The neat part is the loop. We call s.area() without checking what kind of shape s is, and Python automatically runs the right version for each object’s actual type. One small trick: describe() uses self.__class__.__name__, which reads the object’s real class name at runtime, so the same parent method prints “Circle”, “Rectangle”, or “Triangle” correctly. That “call the same method, get the right behavior” idea is polymorphism, and it gets its own detailed walkthrough in the polymorphism tutorial.
The Catch: Forgetting super().__init__()
This is the one mistake almost everyone makes at least once. Imagine a friend, say Anvi, assembling a bookshelf but skipping the base frame step in the manual: the shelf looks perfectly fine leaning against the wall, right up until she puts a book on it. Forgetting super().__init__() fails the same way. When a child class writes its own __init__, that new constructor completely takes over. If you forget to call super().__init__() inside it, the parent’s constructor never runs, and every attribute the parent was supposed to set up just quietly goes missing. No error when the object is created. The crash only shows up later, when something tries to read one of those parent attributes.
📄 catch_no_super.py: parent attributes silently missing
class Base:
def __init__(self):
self.base_attr = "I'm from Base"
class Child(Base):
def __init__(self):
# Forgot super().__init__()!
self.child_attr = "I'm from Child"
obj = Child()
print(obj.child_attr)
try:
print(obj.base_attr) # AttributeError!
except AttributeError as e:
print(f"Error: {e}")
# Fix: always call super().__init__()
class ChildFixed(Base):
def __init__(self):
super().__init__()
self.child_attr = "I'm from Child"
obj2 = ChildFixed()
print(obj2.base_attr) # Works!
print(obj2.child_attr)
▶ Output
I'm from Child Error: 'Child' object has no attribute 'base_attr' I'm from Base I'm from Child
What happened here: Child set its own child_attr but never called super().__init__(), so Base.__init__ never ran and base_attr was never created. Reading obj.child_attr works fine, but the moment we touch obj.base_attr Python raises AttributeError. The fix is one line: ChildFixed calls super().__init__() first, the parent sets up base_attr, and now both attributes exist. The rule writes itself: if a child class defines __init__, call super().__init__() inside it, usually as the first line.
When You Will Use This
Python inheritance is not just a textbook exercise. You will reach for it constantly once you start using real libraries. Here are three places it shows up almost immediately.
- Custom exceptions. When you write
class PaymentError(Exception), you are inheriting from Python’s built-inExceptionclass. You get all the exception machinery for free and only add what makes your error special. Ask any working developer, like our author Rahul: almost every project he has shipped carries a small tree of these custom exception classes. - Web framework views. Frameworks like Django give you a
Viewbase class. You writeclass ProfilePage(View)and override just the methods you care about, likeget(). The framework handles request routing, you handle the page. That is hierarchical inheritance doing the heavy lifting. - Models and forms. Object-Relational Mapping (ORM) and validation libraries hand you a base
ModelorBaseModelclass. YourUserorOrderclass inherits from it and gets saving, loading, and validation without you writing any of that plumbing. You will see this pattern again when the series covers SQLAlchemy and Pydantic.
Common Mistakes
Mistake 1: Deep inheritance hierarchies
If you find yourself building an inheritance chain five levels deep, stop and rethink it. Deep hierarchies are painful to read, painful to debug, and painful to change, because a tweak near the top ripples through everything below. When the relationship is not a genuine “is-a”, reach for composition (a class that “has a” helper object) instead. Two or three levels is plenty for almost everything you will build.
Mistake 2: Using inheritance for code reuse only
Inheritance should model an “is-a” relationship. A Dog is an Animal. A Car is not an Engine. If you just want to share utility methods, use composition or mixins instead of forcing an inheritance relationship that does not exist.
Best Practices
- DO always call
super().__init__()in child constructors - DO use inheritance for genuine “is-a” relationships
- DO keep hierarchies shallow (2-3 levels max)
- DON’T inherit just to share utility code. Use composition for that
- DON’T override methods without calling
super()unless you intentionally want to replace (not extend) behavior
Conclusion
Python inheritance lets you build specialized classes on top of general ones, so you write the common code once and reuse it everywhere. Single inheritance handles the vast majority of real cases, super() keeps the parent’s behavior intact while you extend it, and multi-level and hierarchical chains let you model real-world relationships cleanly. If you remember one thing from this post, make it this: when a child class has its own __init__, always call super().__init__() inside it.
Single and multi-level inheritance follow one tidy chain. Things get more interesting when a class pulls from two or more parents at once. That is multiple inheritance, and it forces Python to decide a clear order for resolving methods, the MRO. We unpack exactly how that works in the multiple inheritance tutorial next. And if you want the full roadmap from Python basics all the way to AI/ML, browse the Python + AI/ML tutorial series home.
Practice Exercises
- Exercise 1: Create Vehicle base with Car and Truck subclasses.
- Exercise 2: Override Animal.speak() in Dog/Cat. Use super().
- Exercise 3: Build a plugin system with setup/execute/teardown and 3 implementations.
Frequently Asked Questions
What is inheritance in Python?
Python inheritance lets a child class reuse attributes and methods from a parent class. The child inherits everything and can add new behavior or override existing methods. It models ‘is-a’ relationships: a Dog is an Animal, an ElectricCar is a Vehicle.
What does super() do in Python?
super() returns a proxy to the parent class, letting you call its methods. super().__init__() initializes parent attributes. super().method() calls the parent’s version of a method so you can extend it instead of replacing it entirely.
What is the difference between single and multiple inheritance?
Single inheritance: one parent, one child (Dog inherits from Animal). Multiple inheritance: one child inherits from two or more parents (FlyingFish inherits from Flyable and Swimmable). Python supports both, but multiple inheritance adds complexity, so see the multiple inheritance tutorial.
Should I use inheritance or composition?
Use inheritance for ‘is-a’ relationships (Dog IS an Animal). Use composition for ‘has-a’ relationships (Car HAS an Engine). When in doubt, prefer composition, since it is more flexible and easier to change later.
What is method overriding in Python?
When a child class defines a method with the same name as the parent, the child’s version takes priority. Calling dog.speak() runs Dog’s speak(), not Animal’s. You can still access the parent’s version with super().speak().
Interview Questions on Python Inheritance
If you can walk through these without peeking, you are ready for this topic in an interview.
Q: You create buddy = Dog("Buddy", "Labrador") where Dog inherits from Animal. Calling buddy.fetch() works, but buddy.info() crashes with AttributeError: 'Dog' object has no attribute 'name'. What do you check first?
Check whether Dog.__init__ calls super().__init__(). When a child defines its own __init__, it fully replaces the parent constructor, so attributes like name that Animal sets up are never created. The object builds without any error, and the crash only appears later when an inherited method reads the missing attribute. The fix is one line: call super().__init__(...) inside the child constructor, usually first.
Q: When a parent and child both define the same method, how does Python decide which one runs?
Python follows the Method Resolution Order (MRO): it looks at the object’s own class first, then walks up the inheritance chain, ending at the built-in object class. The first class that defines the method wins, which is why a child’s override shadows the parent’s version. You can inspect the exact search order with Dog.__mro__ or Dog.mro(). For single and multi-level inheritance it is just a straight line from child to ancestor.
Q: A teammate wrote class ReportGenerator(EmailSender) just to reuse its send() helper. Now every report object drags along SMTP settings it never uses. What would you refactor and why?
This is inheritance used purely for code reuse, and the “is-a” test fails: a report generator is not an email sender. Refactor to composition: give ReportGenerator an email_sender attribute (a “has-a” relationship) and call self.email_sender.send() when needed. The report class stops inheriting unrelated state, the coupling drops, and you can swap the sender for a mock in tests without touching the class hierarchy.
Q: With class Dog(Animal), why does isinstance(buddy, Animal) return True while type(buddy) is Animal returns False? Which should you use?
type(buddy) returns the exact class, which is Dog, so comparing it with Animal fails. isinstance() respects inheritance: it returns True if the object’s class is Animal or any subclass of it. Prefer isinstance() in real code, because it keeps working when someone later adds new subclasses, which is exactly what inheritance is designed to allow.
Q: A new developer adds Truck(Vehicle) and overrides describe(), but the output loses the year, make, and model that the parent version printed. What is the one-line fix?
Inside the override, start with base = super().describe() and build the Truck-specific text on top of that string. Overriding without calling super() replaces the parent behavior entirely, while calling it first turns the override into an extension. This “extend, do not replace” pattern is the standard way to add detail without duplicating the parent’s formatting logic.
Q: Why do experienced Python developers keep inheritance hierarchies to two or three levels?
Deep chains make behavior hard to trace: to understand one method call you may have to read five classes, and a change near the top ripples through everything below it. They also make super() call chains fragile, since one missing link silently skips initialization for a whole level. When a hierarchy grows past three levels, it is usually a sign that some of those relationships are not genuine “is-a” links and should be flattened into composition.
Go deeper: when you outgrow this post, the official Python documentation is the next stop.
Related Posts
Previous: Python: Instance Methods, Class Methods, Static Methods
Next: Python: Multiple Inheritance & MRO
Series Home: Python + AI/ML Tutorial Series

No comment