Python
How Inheritance Works in Python and When to Use It
Understand Python's inheritance mechanics—MRO, super(), and multiple inheritance—and learn practical guidelines for when inheritance is the right tool versus when composition or abstract base classes are better.
June 2026 · 7 min read · 1 views · 0 hearts
Advertisement
How Inheritance Works in Python and When to Use It
Inheritance sounds like a fancy theoretical concept, but you've been using it every time you write class MyView(View) in Django or class MyException(Exception). It's Python's way of letting one class borrow—and customize—another class's behavior.
But here's the thing: inheritance is powerful, and it's also easy to misuse. Let's cut through the abstract OOP lessons and look at how Python actually handles it, and—more importantly—when you should reach for it.
The Mechanics: What Python Does Under the Hood
When you write:
class Animal:
def speak(self):
return "..."
class Dog(Animal):
def speak(self):
return "Woof!"
Python doesn't copy Animal into Dog. Instead, it sets up a method resolution order (MRO). When you call dog.speak(), Python checks Dog first, finds the method, and uses it. If Dog didn't have speak, Python would walk up to Animal and use the parent's version.
The magic is in __mro__:
print(Dog.__mro__)
# (<class '__main__.Dog'>, <class '__main__.Animal'>, <class 'object'>)
That tuple is the exact lookup path. Python tries each class in order until it finds the attribute. This is linear, predictable, and surprisingly fast.
The super() function
super() is not a magic keyword—it's a function that returns a proxy object that follows the MRO. When you call super().speak(), it's basically saying "skip the current class and find speak in the next class up the chain."
class Dog(Animal):
def speak(self):
parent_sound = super().speak()
return f"{parent_sound} But louder: Woof!"
This works even with multiple inheritance, which brings us to...
Multiple Inheritance: The Diamond Problem and Python's Solution
Python allows a class to inherit from multiple parents:
class Flying:
def move(self):
return "Flies"
class Swimming:
def move(self):
return "Swims"
class Duck(Flying, Swimming):
pass
Which move does Duck use? It follows the C3 linearization algorithm—the same one that creates the MRO. The rule is simple: children come before parents, and the order you list the parents matters.
print(Duck.__mro__)
# Duck -> Flying -> Swimming -> object
So Flying.move wins. If you want Swimming.move, either swap the order or explicitly call it.
This is where Python differs from many languages. The diamond problem (where two parent classes share a common ancestor) is handled gracefully—super() in a diamond pattern only calls each class once, in MRO order.
When Inheritance Actually Makes Sense
Here's the hard truth: most "real-world" inheritance examples in tutorials are terrible. A Square inheriting from Rectangle? That breaks the Liskov Substitution Principle (a square is not substitutable for a rectangle if you can set width and height independently).
Use inheritance when:
1. You're building a framework or library
Django's class-based views are the poster child. ListView inherits from BaseListView, which inherits from View, which inherits from object. The framework provides a skeleton, and you override specific methods (get_queryset, get_context_data). The inheritance is invisible to end users—they just see clean, reusable code.
2. You need a hierarchy of related types that share behavior
E-commerce: Product, DigitalProduct, PhysicalProduct. Digital products don't need shipping weight, but both have calculate_price(). The base class provides defaults, subclasses override specifics.
class Product:
def calculate_price(self):
return self.base_price * 1.1 # tax
class DigitalProduct(Product):
def calculate_price(self):
return self.base_price # no tax for digital goods
This is clean because it models a real-world "is-a" relationship. A digital product is a product, just with different pricing.
3. You're using mixin classes
Mixins are small, focused classes that provide a single behavior. They're not meant to stand alone—they're meant to be combined via multiple inheritance.
class JSONMixin:
def to_json(self):
import json
return json.dumps(self.__dict__)
class LogMixin:
def log(self, message):
print(f"[{self.__class__.__name__}] {message}")
class User(JSONMixin, LogMixin):
def __init__(self, name):
self.name = name
Mixins are a great use of multiple inheritance because each does one thing well, and they don't collide with each other's methods.
When NOT to Use Inheritance
- To reuse a single method. Just call the other class directly or use composition.
- When the relationship is "has-a," not "is-a." A car has an engine, it is not an engine.
- When you're creating deep inheritance chains (more than 2–3 levels). They become impossible to debug.
- When you could use a dataclass or a simple function instead. Inheritance adds runtime overhead and complexity.
The Composition Alternative
Often, composition beats inheritance:
class Engine:
def start(self):
return "Vroom"
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
return self.engine.start()
This gives you flexibility—you can swap the engine type at runtime, test components independently, and avoid the rigid coupling of inheritance.
Real Pythonic Advice
Python's collections.abc module (Abstract Base Classes) gives you the best of both worlds: you can inherit from MutableMapping to guarantee your class behaves like a dict, without actually inheriting the dict implementation. You just implement the required methods.
from collections.abc import MutableMapping
class MyDict(MutableMapping):
def __init__(self):
self._data = {}
def __getitem__(self, key):
return self._data[key]
def __setitem__(self, key, value):
self._data[key] = value
# ... and a few more methods
This is the Pythonic sweet spot: you get the contract (inheritance for interface) without the implementation baggage.
The Bottom Line
Inheritance in Python is a tool, not a philosophy. It excels when you have a clear hierarchy of related types, when you're extending framework code, or when you're composing behaviors with mixins. Use it sparingly, favor shallow hierarchies, and when in doubt—reach for composition or abstract base classes.
Python's MRO is clean and predictable, but that doesn't mean you should build a 10-level inheritance tree just because you can. Keep it simple, keep it shallow, and your future self will thank you.
Advertisement
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.