Why this matters
Many types are variations of one another: a savings account is an account with interest; a FileNotFoundError is a kind of OSError; a square is a kind of rectangle. Inheritance lets a class reuse and extend another class's code, and lets code written for the general type work with the specific ones. It is also one of the most overused tools in programming. This lesson shows how inheritance works in Python (including the method resolution order behind super()), how to define interfaces with abstract base classes, and when to prefer composition, building objects out of other objects, instead.
Subclassing
Put the parent class (the base class or superclass) in parentheses:
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def describe(self):
return f"{self.owner}: {self.balance:.2f}"
class SavingsAccount(Account):
def __init__(self, owner, balance=0, rate=0.02):
super().__init__(owner, balance) # let Account set up its part
self.rate = rate
def add_interest(self):
self.deposit(self.balance * self.rate)
def describe(self): # override
return super().describe() + f" (savings at {self.rate:.0%})"
s = SavingsAccount("Ada", 100)
s.add_interest()
print(s.describe())
print(isinstance(s, Account), issubclass(SavingsAccount, Account))
Ada: 102.00 (savings at 2%)
True True
What happened:
SavingsAccountinheritsdepositfromAccount: lookup ofs.depositchecks the instance, thenSavingsAccount, thenAccount, where it is found.- It adds
add_interestand therateattribute. - It overrides
describeby defining a method with the same name; lookup finds the subclass version first. super().describe()calls the parent's version, so the override extends rather than replaces it.super().__init__(owner, balance)runs the parent initializer. If you forget it,self.ownerandself.balanceare never set, and you getAttributeErrorlater.
Every class without an explicit base inherits from object, which supplies defaults such as __repr__, __eq__ (identity) and __init__.
Dynamic dispatch
Methods are looked up on the actual object at run time. A base-class method that calls self.something() gets whatever the subclass provides:
class Report:
def render(self):
return self.header() + "\n" + self.body()
def header(self):
return "REPORT"
def body(self):
return "(empty)"
class SalesReport(Report):
def body(self):
return "sales: 42"
print(SalesReport().render())
REPORT
sales: 42
render is written once; subclasses fill in the parts. This is how frameworks let you customize behavior by overriding a method.
isinstance(obj, Cls) is true for instances of Cls and its subclasses. Prefer it over type(obj) is Cls, which rejects subclasses. Both accept a tuple: isinstance(x, (int, float)).
The MRO and what super() really does
Python allows multiple inheritance: class D(B, C). To decide where lookup goes, each class has a method resolution order, computed once when the class is created (by an algorithm called C3 linearization):
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print([k.__name__ for k in D.__mro__])
['D', 'B', 'C', 'A', 'object']
The MRO lists each class once, children before parents, and keeps the left-to-right order of bases. Attribute lookup walks this list.
super() does not mean "my parent class". It means "the next class after the current one in the MRO of type(self)". In a diamond like the one above, super() inside B goes to C when the object is a D, so every class's method runs exactly once, provided each class calls super(). That is called cooperative inheritance. In single-inheritance code (most code) the next class simply is the parent, and you can think of it that way.
Behind the scenes, the zero-argument super() works because the compiler gives methods defined in a class a hidden reference to that class (__class__), and super combines it with self.
Abstract base classes
Sometimes a base class exists only to define an interface: "every shape has an area". The abc module enforces it:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
...
def describe(self):
return f"area {self.area():.1f}"
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14159 * self.r ** 2
print(Circle(1).describe())
Shape()
area 3.1
TypeError: Can't instantiate abstract class Shape without an implementation for abstract method 'area'
A subclass that forgets to implement area fails the same way when instantiated, which catches the mistake early rather than when area is finally called. The ... (Ellipsis) is a conventional "no body here" placeholder, like pass. The standard library defines many ABCs in collections.abc (Iterable, Mapping, Sequence); isinstance(x, collections.abc.Iterable) asks "does x support iteration?".
Remember also that Python mostly relies on duck typing: if an object has the methods you call, it works, whatever its class. ABCs are for when you want the interface written down and checked.
Composition: has-a versus is-a
Inheritance models is-a relationships. Use it only when the subclass truly is a specialized version that can be used anywhere the base is expected (this is called the Liskov substitution principle). Otherwise, use composition: the object has another object and delegates to it.
A classic mistake is class Stack(list). It saves a few lines, but a stack is not a list: users can now call insert(0, x), sort(), or index into the middle, all of which violate stack rules, and you can never change the internal representation without breaking them. With composition:
class Stack:
def __init__(self):
self._items = [] # has-a list
def push(self, x):
self._items.append(x)
def pop(self):
return self._items.pop()
The public interface is exactly what you chose. Other signs that composition fits: you want to swap the part at run time (a Car has an Engine, electric or petrol), or you would need to inherit from two unrelated things. A short rule: favor composition; inherit when the is-a relationship is real and you want polymorphism.
Common mistakes
- Forgetting
super().__init__(...): parent attributes are missing, causingAttributeError: 'SavingsAccount' object has no attribute 'balance'. - Calling the parent by name in multiple-inheritance code (
Account.__init__(self, ...)): it skips the MRO and can run a class twice or not at all. Usesuper(). - Changing a method's signature when overriding, so code written for the base class breaks when handed a subclass.
- Deep hierarchies (five levels of inheritance) are hard to follow: to understand one method you must read every class in the MRO. Keep hierarchies shallow.
Recap
class Child(Parent):inherits attributes; defining a same-named method overrides;super().method()reaches the next implementation.- Lookup follows the MRO (
Cls.__mro__);super()means "next in the MRO of the object's type", enabling cooperative multiple inheritance. abc.ABCand@abstractmethoddefine interfaces that cannot be instantiated until implemented.- Prefer composition (has-a) unless the is-a relationship is real.
Going further: print bool.__mro__ and FileNotFoundError.__mro__ to see real hierarchies, and read the abc and collections.abc module docs.