In October 2020 I took Dave Beazley’s Advanced Programming with Python course. Since I wrote a series about his SICP course and the Raft course, I figured I’d write about this one too :).
In the last post of this series, we talked about interface implementation in Python. This time, we’ll talk about inheritance—and the things that make inheritance in Python unusual.
Most programming books depict inheritance as a tree, with subclasses stemming from the superclasses that they extend.
So a subclass inherits attributes and behavior from superclasses, the way a child might inherit physical attributes from a parent.
Here’s where that analogy breaks down, though: most languages strictly support single inheritance: that is, a class can only inherit from one other class. That forces us into conceptualizing inheritance not as a tree, but as a set of concentric circles, with each subclass occupying a smaller circle inside that of its superclass.
In languages with single inheritance, each time we make a subclass, we’re making a wager that two things are true:
- What’s true of the superclass is true of the subclass.
- The subclass is only a special case of this superclass; not of anything else.
That’s a pretty bold wager, and we often get it wrong. When we get it wrong and leave the inheritance structure intact, we have to do weird stuff like:
- Override superclass methods with no-ops (when things that are true of the superclass are not true of the subclass)
- Include modules or inject dependencies (when the subclass is a special case of something besides its superclass. This isn’t weird in and of itself, but you can end up in discussions about which thing should be the superclass and which should be the dependencies that force you to model the problem space in arbitrary, inaccurate ways).
Python is one of the languages that supports the imperative paradigm with multiple inheritance, which allows a class to subclass multiple other classes:
class Raptor: def hunt(self, prey): ... class Nocturn: def echo_locate(self): ... class Owl(Raptor, Nocturn): ...
Allow me to name the elephant in the room: people will tell you not to do this. Their reasoning usually boils down to “Inheritance is bad, so moar inheritance must be moar bad.” I have limited patience for the “inheritance is bad” canard, and generally, all rumors that rely on arbitrary assumptions about a fictional universal case. So, other than this acknowledgment, I’m not going to entertain “inheritance is bad” in this piece.
What do we get from multiple inheritance?
Allowing classes to inherit from multiple other classes frees us up to build structures that resemble generational family trees, with multiple ancestors. This de-risks the choice to make a subclass because the subclass does not have to be a special case of only this thing. If a class needs to inherit several behaviors, but different subclasses need those behaviors mixed and matched, then we can divide those behaviors into separate superclasses and have each subclass inherit only the relevant ones.
It fits with the Python philosophy that the language would allow multiple inheritance. A language that allows multiple inheritance also allows single inheritance, and Python often strives for a single implementation that encompasses all use cases. We saw another example of this in a recent piece where we looked at abstract classes.
Multiple inheritance introduces a challenge, though: method resolution order.
Suppose you have this code:
class Bird: def make_sound(self): return "Tweet?" class Corvid(Bird): pass class Nuisance: def make_sound(self): return "HAHA I STOLE YOUR FRENCH FRY!" class BlueJay(Corvid, Nuisance): pass
What happens when I call
The answer depends on how Python decides which of the two
make_sound implementations gets precedence in the Blue Jay inheritance tree. We call this decision the method resolution order, and Python’s checks the entire inheritance chain of the first (or leftmost) superclass before moving onto the next one. You can examine a Python class’s method resolution order by calling the
.mro() method on the class:
So, contrary to everything we know about blue jays, the answer here is “Tweet?”
In order to produce a more accurate representation of the bird, we have to switch the order of the classes from which
BlueJay inherits, so that
Nuisance is the leftmost one:
Bidirectional Tree Traversal in Python Inheritance
Python’s inheritance implementation has another distinguishing characteristic; in a field of imperative languages that allow influence to flow only down the chain of inheritance, Python allows subclasses to send notifications back up.
To explain how this is done, we can look at a seemingly unrelated example that exemplifies the use of the
__init_subclass__ method, which allows a superclass to set attributes, invoke wrappers, and otherwise mess with a subclass at initialization time.
class Bird: def __init_subclass__(cls, call, **kwargs): super().__init_subclass__(**kwargs) cls.make_sound = lambda: print(call) class Crow(Bird, call="Ca-caw!"): pass
This code, for example, sets a class method called
make_sound on any subclass of
Bird. That method prints out the
call attribute, which we set upon the class’s construction (not its initialization, which happens later).
The above code means that we can call
make_sound on the
Crow class and expect it to print the crow’s sound:
Now, doing this doesn’t make a whole lot of sense. We might ask an individual instance of a crow to make a sound, but why would we want to ask the entire concept of a crow to make a sound? We wouldn’t. This is not a realistic example of how we would use
But we might use it, instead, to build a registry at the superclass level:
class Bird: types =  def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) cls.types.append(cls.__name__)
Bird.types gives us an empty list. But once we make some subclasses…
class Crow(Bird): pass class Hawk(Bird): pass class Flamingo(Bird): pass
…they all get registered with the superclass and appear in the
This built-in listener can prove super useful when, for example, we’re building a generic framework that developers use to subscribe to messages. If those developers create classes that inherit from the superclass and the superclass receives a message, it can then call a specific method on each of its subclasses to keep them appraised.
Prior to Python Enhancement Proposal (PEP) 487, Pythonistas did this with metaclasses. But the maintainers, true to form, settled instead on this syntax, which fit within the existing class definition paradigm and presented superclass subscription syntax as a special case to be handled by its own method, rather than a separate pattern altogether.
Now that we’ve covered interfaces and inheritance, it’s almost time to talk about elevators. But this post is getting a bit long, so we’ll save that for the next one 🙂
If you liked this piece, you might also like:
The Philosophy of Software Design series
Techtivism! About how to wield your role as a technologist in accordance with your values.
The series on Bob Nystrom’s…book? Open education project? Anyway, it’s called Crafting Interpreters, and by the time you read the existing pieces I’ll be updating it again with new posts.