Definition
Two classes often end up looking very much alike. A user and an administrator carry the same email and introduce themselves the same way; only a handful of powers separate them. Copying the first one to write the second works on the day, then jams at the first fix: it has to be applied twice, and forgetting one file goes unnoticed.
Inheritance solves exactly that annoyance. It is a way of writing one class from another: the new one picks up the attribute and the method of the one it descends from, and holds only what sets it apart. The first is called the parent class, or base class; the one inheriting is called the subclass.
It all fits in a pair of brackets placed after the name of the new class.
class User:
def __init__(self, email):
self.email = email
def introduce(self):
return f"Account {self.email}"
class Administrator(User):
def delete(self, resource):
...Administrator says nothing about the email, and yet every instance owns one and knows how to introduce itself. The only code left to type is the one that did not exist on the parent, and a fix made to User immediately benefits everything descending from it.
The three moves of a subclass
Once the ancestry is set, the same question comes back in front of every parent method: keep it, replace it, or add another one beside it? Three answers, not one more, and having them in mind clears up half of the hesitations.
| Move | What happens |
|---|---|
| Inherit | The parent method serves as it stands, with no line to rewrite |
| Add | A brand new method exists on the subclass and not on the parent |
| Override | A method of the same name replaces the parent one for this class |
The third move is the one that surprises. Python merges nothing and warns about nothing: on the call, it looks the name up on the class of the object, climbs from parent to parent, and stops at the first match. An introduce method written in Administrator therefore hides the one on User without deleting it. The diagram follows that path across three classes.
That climb never ends in thin air: every class descends from object, including the ones written without brackets. This is where the behaviours an object owns before the first line written for it come from.
super, and the forgotten initialisation trap
Adding an attribute to the subclass means overriding __init__, and that is the most common case. Now overriding means replacing: the parent version is no longer called, so the attributes it used to install are no longer installed. It has to be called with super, before adding whatever belongs to the subclass alone.
class Administrator(User):
def __init__(self, email, level):
super().__init__(email)
self.level = levelThe line super().__init__(email) has the parent run the work it already knows how to do, without copying an instruction of it. It comes first, because the rest of the method usually depends on it.
Forgetting that call raises no error: the object is built normally, in silence. The gap only shows up on the first access to self.email, as an AttributeError raised far from the guilty file. It is mistake number one when starting with objects.
Inherit or compose
Inheritance is often picked for a bad reason: two classes share some code. Sharing code is not an ancestry, though. The test that settles it comes as a question: is the subclass genuinely a kind of parent? An administrator is a user, so inheritance is justified. An order is not a list of products, it contains one: the answer is an attribute, which is what composition means.
The reason for that caution is coupling. A subclass leans on the internal details of its parent, including the ones the parent promises to nobody. Rename an internal method, and code written elsewhere breaks, sometimes months later.
Python adds a reason not to inherit by reflex: it requires no ancestry at all to accept an object. A function calling introduce is happy with any object owning that method. This behaviour has a name, duck typing.
Frequently asked questions
Can a class inherit from several parents?
Yes, by listing the parents inside the brackets. Python consults them from left to right, following an order called the MRO, which MyClass.__mro__ prints as it stands. In practice this multiple inheritance stays reserved for small, very focused classes: as soon as the same name exists on two parents, guessing which one wins costs the next reader an effort.
How can the ancestry of an object be checked?
With isinstance(obj, User), which answers true for the parent as well as for all of its subclasses. A strict type comparison would answer false for an administrator, even though an administrator really is a user. That is therefore the check to favour as soon as a hierarchy exists.
Is inheriting from a standard library class allowed?
Yes, and the most useful case is building custom exceptions by inheriting from Exception: calling code then catches that precise error instead of catching everything. A dataclass can serve as a base too. The Python course walks through that progression on a full project, from the first object to the class hierarchy.