Python Inheritance: Syntax, super(), and MRO
python inheritance: Learn how Python inheritance works: subclass syntax, overriding methods, super(), MRO, multiple inheritance, and common pitfalls.
Python inheritance is a core language feature that lets a class reuse and extend the behavior of another class. It is one of the first mechanisms developers reach for when they want to share logic between related types, and it is also one of the easiest to misuse when the class hierarchy grows beyond a few levels.
The Basic Syntax of Python Inheritance
Defining a subclass in Python is straightforward. You place the parent class name in parentheses after the child class name:
class Animal: def __init__(self, name): self.name = name def speak(self): return f"{self.name} makes a sound" class Dog(Animal): def speak(self): return f"{self.name} barks"
Here, Dog inherits the __init__ method and the name attribute from Animal, but overrides speak with its own implementation. The child class can also add new methods or attributes without affecting the parent. This is the simplest form of reuse: the child gets everything the parent has, and you only change what needs to differ.
Inheritance is not limited to one level. A class can inherit from another subclass, creating a chain. The lookup for attributes and methods walks up that chain until it finds a match or raises AttributeError.
Overriding Methods and Extending Parent Behavior
Overriding a method is common, but often you want to keep the parent's behavior and add to it. The super() function gives you access to the next class in the method resolution order (MRO), which is usually the parent class. This lets you call the original implementation:
class Dog(Animal): def speak(self): return super().speak() + " but louder"
Now Dog.speak() first calls Animal.speak() and then appends text. This pattern is useful when you need to extend the parent's logic without duplicating it. It also keeps the parent's internal state consistent if the method modifies instance attributes.
A common mistake is to forget to call super().__init__() when overriding __init__. If the parent class sets up required attributes, skipping the call leaves the instance incomplete. For example:
class Dog(Animal): def __init__(self, name, breed): self.breed = breed # Missing super().__init__(name)
This Dog instance will not have a name attribute, and any method that relies on it will fail. Always call super().__init__() unless you have a specific reason not to, and if you do, document why.
Using super() to Cooperate with Parent Classes
super() does more than call the immediate parent. It returns a proxy that delegates method calls to the next class in the MRO. This becomes critical in multiple inheritance, where a class can have several ancestors. Consider this example:
class A: def __init__(self): print("A.__init__") super().__init__() class B: def __init__(self): print("B.__init__") super().__init__() class C(A, B): def __init__(self): print("C.__init__") super().__init__()
When you create C(), the output is:
C.__init__
A.__init__
B.__init__
Even though A does not inherit from B, super() inside A.__init__ continues to the next class in the MRO, which is B. This cooperative behavior works because every class in the chain uses super() consistently. If any class skips super().__init__(), the chain breaks and later classes never get initialized.
This pattern is powerful but requires discipline. Every class in a multiple-inheritance hierarchy must cooperate by calling super() in its methods, and the signatures should be compatible. The MRO is computed using the C3 linearization algorithm, which ensures that each class appears before its parents and that the order respects the left-to-right order of bases.
Method Resolution Order and Multiple Inheritance
Python's MRO determines the order in which classes are searched for attributes and methods. You can inspect it with the __mro__ attribute:
class A: pass class B(A): pass class C(A): pass class D(B, C): pass print(D.__mro__) # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
The MRO for D is D -> B -> C -> A -> object. This ordering ensures that B is checked before C, and both before A. The C3 algorithm guarantees that the order is consistent and that no class appears before a class it inherits from.
Multiple inheritance can solve real problems, but it also introduces complexity. The diamond problem—where a class inherits from two classes that share a common ancestor—is handled by the MRO, but the behavior can still surprise developers who expect a different order. For example, if B and C both override a method from A, D will use the one from B because B appears first in the MRO. If you need to call a specific parent's method, you can do so directly by naming the class, but that breaks the cooperative pattern and should be used sparingly.
Abstract Base Classes and Interface Contracts
Sometimes you want to define a class that cannot be instantiated directly, but instead serves as a contract for subclasses. Python's abc module provides ABC and abstractmethod for this purpose:
from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): pass class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return 3.14159 * self.radius ** 2
Any subclass of Shape must implement area, or it will also be abstract and cannot be instantiated. This is a way to enforce an interface without relying on runtime checks. It also documents the expected behavior for developers who read the code.
Abstract base classes are useful when you have a family of related classes that share a common API but differ in implementation. They are the Python equivalent of interfaces in languages like Java, though they can also contain concrete methods that subclasses inherit.
Common Inheritance Pitfalls and How to Avoid Them
Inheritance is easy to misuse. One common pitfall is using it for code reuse when composition would be a better fit. For example, a Car class might inherit from Engine to get its start() method, but a car is not a type of engine. This creates a fragile hierarchy that breaks when the engine changes. Prefer composition: give Car an engine attribute and delegate to it.
Another pitfall is overriding methods without considering the parent's invariants. If the parent class maintains internal state, an override that skips the parent's logic can corrupt that state. Always call the parent's method when the override is meant to extend, not replace.
Deep inheritance hierarchies are also a maintainability problem. A chain of five or six classes makes it hard to trace where a method is actually defined. The MRO becomes difficult to reason about, and changing a method in a distant ancestor can have unexpected effects on many subclasses. This is known as the fragile base class problem. Keep hierarchies shallow and prefer composition when the relationship is not a clear "is-a" one.
Finally, be careful with super() in multiple inheritance. If any class in the hierarchy does not call super() in a method, the cooperative chain breaks. This is especially common with __init__ when a class has no parent but still calls super().__init__()—that is fine because it reaches object, but if a class forgets to call it, the next class in the MRO never gets initialized.
Performance and Maintainability Considerations
Inheritance has minimal runtime overhead in Python. Attribute lookup uses the MRO, which is computed once per class and cached, so the cost of a method call is essentially the same as a regular function call. There is no performance reason to avoid inheritance for a well-designed hierarchy.
Maintainability is a different story. Inheritance creates a strong coupling between classes. A change in a parent class can ripple through all subclasses, and the more levels there are, the harder it is to predict the impact. This is why many style guides recommend favoring composition over inheritance unless there is a genuine "is-a" relationship.
When you do use inheritance, keep the hierarchy flat and each class focused. Use abstract base classes to define contracts, and use super() consistently to support cooperative multiple inheritance. If you find yourself overriding many methods just to change small behaviors, consider whether a strategy or template method pattern would be cleaner.
One concrete technique to reduce coupling is to use super().__init__() with keyword arguments that are explicitly accepted by each class. This allows each class to pick the parameters it needs without requiring a common signature. For example:
class Base: def __init__(self, **kwargs): self.name = kwargs.pop("name") super().__init__(**kwargs) class Mixin: def __init__(self, **kwargs): self.extra = kwargs.pop("extra", None) super().__init__(**kwargs) class Child(Base, Mixin): def __init__(self, **kwargs): super().__init__(**kwargs)
This pattern keeps the initialization chain flexible and avoids the common TypeError caused by mismatched __init__ signatures. It is a practical way to manage multiple inheritance without hardcoding parameter lists.
Inheritance remains a valuable tool when used deliberately. Understand the MRO, use super() consistently, and prefer composition when the relationship is not a true subtype. With those rules, you can build hierarchies that are both expressive and maintainable.