Python Inheritance
Inheritance lets a class (the subclass/child class) reuse the attributes and methods of another class (the superclass/parent class), while adding new behaviour or overriding existing behaviour. It models an “is-a” relationship (a DeliveryRobot is a Robot) — contrast with python-composition’s “has-a” relationship.
Syntax
class Parent():
# parent class' implementation
class Child(Parent):
# child class' implementation -- inherits everything from Parent
A subclass automatically gets all of its parent’s attributes and methods. It can:
- Add new methods/attributes that only it has.
- Override an inherited method by redefining it with the same name.
- Extend an inherited method using
super(), rather than fully replacing it.
super()
super().method(...) calls the parent class’ version of a method — most commonly used inside an overridden __init__ to reuse the parent’s initialization logic before adding subclass-specific setup:
class DeliveryRobot(Robot):
def __init__(self, name: str, batt_level: int, load_capacity: int):
super().__init__(name, batt_level) # reuse Robot's __init__
self.load_capacity = load_capacity
Terminology cheat-sheet
Given class Student(Person):
| Term | Refers to |
|---|---|
| subclass / child class | Student |
| superclass / parent class | Person |
“Student inherits from / extends Person” |
the relationship between them |
Polymorphism
Polymorphism (“many forms”) describes an interface that works across different underlying types. Built-ins like len() are polymorphic (works on strings, lists, dicts, …). Classes that share a method name (whether via a common superclass or just by convention) are also polymorphic: the same call, e.g. r.charge(), produces different behaviour depending on which actual class r is an instance of.
When to use inheritance
Only when there’s a genuine “is-a” relationship between the two classes. If the relationship is really “has-a” (one object contains or uses another), prefer python-composition instead.
Abstract base classes
An abstract base class doesn’t provide concrete implementations for some/all of its methods — it just defines the interface that every concrete subclass must implement. You’re not meant to instantiate the abstract class directly.
The simplest (unenforced) version just raises an error from each method that subclasses must override:
class Shape:
def area(self) -> float:
raise NotImplementedError("Subclasses must implement area()")
This only fails when the unimplemented method is actually called — Shape() itself still succeeds. The standard-library abc module enforces this properly, raising TypeError at the point of instantiation:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
# concrete helpers are still allowed alongside abstract methods
def describe(self) -> str:
return f"{self.__class__.__name__}"
With ABC, Shape() raises immediately: TypeError: Can't instantiate abstract class Shape with abstract methods area.
Method Resolution Order (MRO)
When a class inherits from multiple parents (or a chain of parents) that define the same method/attribute name, Python needs a deterministic rule for which one wins. That rule is the Method Resolution Order (MRO) — the order Python searches through classes to resolve a name on an object. It’s stored on the class as cls.__mro__ (or cls.mro()).
Python’s inheritance models
- Single inheritance:
class B(A):— one parent. - Multilevel inheritance:
class C(B):(whereBitself inherits fromA) — a linear chain; the MRO just follows the chain upward. - Hierarchical inheritance: multiple children (
B,D, …) inherit from the same parentA. - Multiple inheritance:
class E(B, D):— a class inherits from more than one parent directly.
The diamond problem and C3 linearisation
If E(B, D) and both B and D inherit from A, which version of A’s methods should E use? Python resolves this with C3 linearisation:
- Child classes are checked before parents.
- Parents are checked in the order they’re listed in the class definition.
- If a class would appear more than once, only its last occurrence is kept.
>>> class E(B, D): pass
>>> [cls.__name__ for cls in E.__mro__]
['E', 'B', 'D', 'A', 'object']
Each attribute/method name is resolved independently by walking this list and taking the first class that defines it — so two different method calls on the same object can effectively “come from” two different classes in the hierarchy.
super() follows the MRO
super() doesn’t call the immediate parent named in the class statement — it calls the next class after the current one in the instance’s actual MRO. This is what makes cooperative multiple inheritance work: as long as every class in the chain calls super(), a single call can ripple through every class in the MRO exactly once, in order.
class A:
def ping(self): print("A")
class B(A):
def ping(self): print("B"); super().ping()
class C(A):
def ping(self): print("C"); super().ping()
class D(B, C):
def ping(self): print("D"); super().ping()
>>> D().ping()
D
B
C
A
If any class in the chain omits its super() call, the chain simply stops there for that call — the MRO itself doesn’t change (it’s purely a function of the class hierarchy), but fewer methods actually get executed.