Python Class Inheritance: Syntax and Design
python class inheritance: Learn how Python class inheritance works: method resolution order, super(), multiple inheritance, overriding, and when to prefer composition.
python class inheritance requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you define a class that reuses behavior from another class, you are using inheritance. In Python, class Child(Parent) creates a subclass that inherits attributes and methods from Parent. The mechanics look simple, but the runtime behavior of inheritance—especially with multiple parents—depends on a specific algorithm and a few rules that are easy to get wrong.
Consider the minimal case:
class Animal: n def speak(self): return "..." class Dog(Animal): def speak(self): return "Woof"
Dog overrides speak and inherits everything else from Animal. This is the the most common use of python class inheritance: extending or specializing an existing class without duplicating code. But the inheritance also affects attribute lookup, __init__ behavior, and method resolution order in ways that matter when you build larger hierarchies.
How Method Resolution Order Works
Every Python class has a Method Resolution Order (MRO), which is determines the order in which base classes are searched when you access a method or attribute. The MRO is computed using the C3 linearization algorithm. You can inspect it with ClassName.__mro__ or ClassName.mro().
For a single-inheritance chain, the MRO is straightforward: the child class first, then its parent, then the grandparent, and so on up to object. For multiple inheritance, the order becomes less obvious.
class A: def who(self): return "A" class B(A): def who(self): return "B" class C(A): def who(self):: return "C" nclass D(B, C): pass print(D.mro()) # [<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>]
The MRO respects the order of bases in the class definition. D checks B before C, and A is reached only after both B and C have been searched. The C3 algorithm guarantees that each class appears exactly once and that a class always appears before its bases. This ordering is what makes cooperative multiple inheritance possible.
Using super() to Delegate to Parent Classes
super() gives you access to the next class in the MRO, not necessarily the immediate parent. In a single-inheritance hierarchy, it is the direct parent. In a multiple-inheritance hierarchy, it is the next class according to the MRO. This is critical for cooperative method calls.
class Base: def __init__(self, value): self.value = value class Mixin: def __init__(self, *args, **kwargs): print("Mixin init") super().__init__(*args, **kwargs) class Child(Mixin, Base): def __init__(self, value): super().__init__(value) print("Child init") c = Child(42) # Mixin init # Child init
Notice that Mixin.__init__ calls super().__init__, which resolves to Base.__init__ because the MRO is Child -> Mixin -> Base. Without the super() call in Mixin, Base.__init__ would never run. This pattern requires every class in the hierarchy to cooperate by calling super() and accepting *args, **kwargs when the signature is not fixed.
Multiple Inheritance and the Diamond Problem
Multiple inheritance creates a diamond when two parent classes share a common ancestor. For example, class D(B, C) where both B and C inherit from A. The MRO ensures that A is only initialized once, but only if every class along the way uses super().
class A: def __init__(self): print("A init") self.a = 1 class B(A): def __init__(self): print("B init") super().__init__() class C(A): def __init__(self): print("C init") super().__init__() class D(B, C): def __init__(self): print("D init") super().__init__() d = D() # D init # B init # C init # A init
The order is D -> B -> C -> A. Each __init__ calls the next in the MRO, so A runs exactly once. If any class in the chain calls the parent directly by name instead of using super(), the diamond breaks and A may be initialized multiple times or not at all.
Overriding Methods and Attributes
Overriding is not limited to methods. You can override any class attribute, including properties and class variables. When you override a method, you often want to extend the parent behavior rather than replace it entirely. That is where super() becomes useful.
class Vehicle: def __init__(self, make): self.make = make def description(self): return f"Vehicle made by {self.make}" class Car(Vehicle): def __init__(self, make, model): super().__init__(make) self.model = model def description(self): return f"{super().description()} - model {self.model}"
Here Car.description calls the parent implementation and then appends extra information. This keeps the parent logic in one place and avoids duplicating the make formatting.
A common mistake is forgetting to call super().__init__() in the child's __init__. If the parent sets attributes that the child methods rely on, those attributes will be missing. The child class will still be instantiated, but attribute access will raise AttributeError at runtime.
Inheritance vs Composition: When to Use What
Inheritance is a relationship where the child is a more specific version of the parent. Composition is a relationship where a class holds an instance of another class and delegates behavior to it. The choice affects maintainability and testability.
Use inheritance when:
- The child genuinely is a subtype of the parent, and you want to reuse the parent's interface.
- You need polymorphic behavior where a function accepts the parent type and works with any child.
- The hierarchy is shallow and unlikely to change frequently.
Use composition when:
- The relationship is more like "has-a" than "is-a". For example, a
Carhas anEngine, but it is not anEngine. - You want to avoid fragile base class problems, where changes in the parent break children.
- You need to swap behavior at runtime or mock dependencies in tests.
Composition is often easier to test because you can replace the composed object with a mock. Inheritance binds the child to the parent's implementation details, making refactoring riskier.
Common Pitfalls and Runtime Behavior
Several runtime behaviors can surprise developers new to python class inheritance.
Mutable class attributes are shared across all instances. If you define a list or dict at the class level and modify it through an instance, the change affects every instance.
class A: items = [] a1 = A() a1.items.append(1) a2 = A() print(a2.items) # [1]
To get per-instance data, assign the mutable object inside __init__.
The __init__ method is not automatically called for the parent. You must call super().__init__() explicitly. If you don't, the parent's initialization is skipped, which can leave the object in an inconsistent state.
Method resolution order is not the same as the order you might expect intuitively. Always check ClassName.mro() when you have a complex hierarchy. The MRO depends on the order of bases in the class definition, not on the order of inheritance statements.
Overriding a classmethod or staticmethod works, but you need to call the parent version explicitly if you want to extend it. The super() proxy works for class methods and static methods as well, but the arguments differ slightly. For a classmethod, super() returns a bound method that passes the class, not the instance.
Maintainability and Design Considerations
Inheritance is a coupling mechanism. A change in a parent class can silently alter the behavior of all subclasses. To keep the hierarchy maintainable, follow the Liskov substitution principle: a child should be usable anywhere the parent is expected, without breaking the program's invariants.
This means you should not override a method to raise an exception that the parent's contract allows, nor should you change the method's signature in a way that violates the caller's expectations. In Python, you can override with a different signature, but the callers that use the parent type may fail at runtime.
A practical rule is to keep inheritance hierarchies shallow. If you find yourself adding many levels of inheritance to share code, consider extracting the shared behavior into a mixin or a separate helper class and using composition instead.
When you do use multiple inheritance, make sure every class in the chain calls super() and accepts *args, **kwargs in its __init__ if the signatures are not identical. This is the only way to guarantee that all initializers run in the correct order.
Finally, remember that inheritance is a compile-time (or class-definition-time) relationship, but the MRO is computed when the class is created. If you dynamically create classes or modify bases, the MRO is recalculated, which can lead to subtle bugs. Prefer static class definitions unless you have a strong reason to use type() to construct classes at runtime.