Inheritance: Pros and Cons
Notes based on Fluent Python, summarized and rephrased in my own words.[file:19]
Why inheritance exists (and why to be careful)
Inheritance was originally promoted to let beginners reuse complex frameworks designed by experts.[file:19] It can:
- Express "is-a" relationships between types.
- Enable polymorphism via method overriding.
- Avoid code duplication by reusing implementations.
But inheritance, especially from built-ins and with multiple bases, can also be fragile and surprising. This chapter focuses on where inheritance hurts and how to design around it.[file:19]
Subclassing built-in types is tricky
Subclassing built-in container types like dict, list, or str is often error-prone because many built-in methods bypass your overrides and call internal C implementations directly.[file:19]
Example: trying to customize item assignment in a dict subclass:[file:19]
class DoppelDict(dict):
def __setitem__(self, key, value):
super().__setitem__(key, [value] * 2)
# store value twice as a list
dd = DoppelDict(one=1)
print(dd) # {'one': 1}
dd['two'] = 2
print(dd) # {'one': 1, 'two': [2, 2]}
dd.update(three=3)
print(dd) # {'one': 1, 'two': [2, 2], 'three': 3}Here:
- Direct assignment
dd['two'] = 2goes through our overridden__setitem__, so'two': [2, 2]is stored.[file:19] - But the constructor call
DoppelDict(one=1)and laterdd.update(three=3)do not use our override; they call the built-in implementation internally and store1and3unmodified.[file:19]
This violates the usual OO expectation that all methods use the most-derived implementation. With many built-ins, their C-level methods directly manipulate internal state and skip dynamic dispatch.
Use collections wrappers instead
Because of this, the recommendation is:[file:19]
- Do not subclass built-in container types (
dict,list,str) directly. - Instead, subclass the "user" wrapper classes in
collections:collections.UserDictcollections.UserListcollections.UserString
These are pure-Python wrappers around the corresponding built-ins and are designed specifically to be easy and safe to extend.[file:19]
import collections
class DoppelDict2(collections.UserDict):
def __setitem__(self, key, value):
super().__setitem__(key, [value] * 2)
dd = DoppelDict2(one=1)
print(dd) # {'one': [1, 1]}
dd['two'] = 2
print(dd) # {'one': [1, 1], 'two': [2, 2]}
dd.update(three=3)
print(dd) # {'one': [1, 1], 'two': [2, 2], 'three': [3, 3]}Here, all operations (__init__, update, item assignment) respect our overridden __setitem__ because UserDict delegates through the instance methods correctly.[file:19]
Multiple inheritance and method resolution order (MRO)
Languages that support multiple inheritance must handle potential name conflicts: different base classes may define methods with the same name.[file:19]
Consider classes A, B, C, and D:[file:19]
class A:
def ping(self):
print('ping:', self)
class B(A):
def pong(self):
print('pong:', self)
class C(A):
def pong(self):
print('PONG:', self)
class D(B, C):
def ping(self):
super().ping()
print('post-ping:', self)
def pingpong(self):
self.ping()
super().ping()
self.pong()
super().pong()
C.pong(self)Questions:[file:19]
- What happens for
d = D()andd.pong()? - Which
pongimplementation is used?
d = D()
d.pong() # uses B.pong
C.pong(d) # explicitly call C.pong with d as selfPython chooses the method based on the Method Resolution Order (MRO), a linear ordering of classes that Python computes for each class.[file:19]
You can inspect it via the __mro__ attribute:
D.__mro__
# (__main__.D, __main__.B, __main__.C, __main__.A, object)Thus, for d.pong(), Python looks in D, then B, then C, then A, then object, so it finds B.pong first.[file:19]
Using super() vs calling base methods directly
The recommended way to delegate to a superclass method is super():
class D(B, C):
def ping(self):
super().ping() # respects MRO
print('post-ping:', self)Sometimes you may intentionally want to bypass MRO and call a specific base-class implementation:
class D(B, C):
def ping(self):
A.ping(self) # explicit call to A.ping
print('post-ping:', self)Note:[file:19]
- When calling a method directly on the class, you must pass
selfexplicitly, because you’re using an unbound method. - Overusing direct base-class calls in complex hierarchies can break the cooperative nature of
super()and lead to fragile class graphs.
Under the hood, Python 3 uses the C3 linearization algorithm to compute MRO. For a thorough treatment, see Michele Simionato’s paper "The Python 2.3 Method Resolution Order".[file:19]
Dealing with multiple inheritance: design guidelines
Multiple inheritance can easily produce confusing and fragile designs. The notes suggest some pragmatic guidelines:[file:19]
Separate interface inheritance from implementation inheritance
- Interface inheritance: creating subtypes that answer "what is it?" (e.g. A
Mappingdescribes a key→value interface). - Implementation inheritance: reusing code to avoid duplication.
- Interface inheritance: creating subtypes that answer "what is it?" (e.g. A
Be clear why you are creating a subclass
- Are you extending or specializing a conceptual type (interface)?
- Or are you just trying to share implementation details?
Use abstract base classes (ABCs) to model interfaces
- ABCs clarify the contract expected from subclasses.
- They can mix interface and some default behavior.
Use mixins to reuse code
- A mixin class exists only to provide method implementations for multiple unrelated subclasses.
- It does not represent an "is-a" relationship on its own.
Name mixins clearly
- Use names that include
Mixinto signal their role (e.g.LoggingMixin,JsonSerializableMixin).
- Use names that include
ABCs can serve as mixins; mixins are not necessarily ABCs
- An ABC may provide default method bodies and can be used like a mixin.
- Some mixins may not define abstract methods at all; they just add behavior.
Avoid subclassing multiple concrete classes
- If you write
class C(A, B, D):, then at most one ofA,B, orDshould be a concrete, full-featured class.[file:19] - The others should be ABCs or mixins, not complete concrete types.
- If you write
Provide composition-based "aggregate" classes to users
- Instead of deep inheritance trees, consider classes that hold other objects and delegate, i.e. composition over inheritance.[file:19]
Prefer object composition over class inheritance
- Composition tends to be more flexible, easier to change, and less entangled than large inheritance graphs.[file:19]
These guidelines aim to limit the complexity that multiple inheritance can introduce, while still letting you benefit from mixins and ABCs where they make sense.