Inheritance lets a class reuse the attributes and methods of another class. You define shared behavior once in a parent, and every child gets it automatically. Dog and Cat both eat and sleep, so you put that in Animal and let them inherit it.
The problem it solves#
Without inheritance, related classes repeat the same code:
# Without inheritance: the same eat() copied into every class
class Dog:
def eat(self):
print("Dog is eating")
class Cat:
def eat(self):
print("Cat is eating")
# With inheritance: eat() is defined once
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name} is eating")
class Dog(Animal):
pass
class Cat(Animal):
pass
How it works: attribute lookup#
Inheritance is just the attribute lookup chain extended upward. Python checks, in order:
instance.__dict__Class.__dict__Parent.__dict__, then each further ancestorobject, the root of every class
class Animal:
def eat(self):
print("eating")
class Dog(Animal):
pass
Dog().eat()
# not in the instance, not in Dog.__dict__, found in Animal.__dict__
Single inheritance and overriding#
A child class inherits everything and can replace any method:
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name} is eating")
class Dog(Animal):
def bark(self): # new behavior
print(f"{self.name} is barking")
d = Dog("Bruno")
d.eat() # inherited
d.bark() # Dog's own
A child can override a parent method by defining one with the same name. Lookup finds the child's version first and never reaches the parent's.
class Dog(Animal):
def eat(self):
print(f"{self.name} is eating like a dog")
super(): extend instead of replace#
Call super() when you want the parent's behavior plus your own:
class Dog(Animal):
def eat(self):
super().eat() # Animal's version first
print(f"{self.name} is eating like a dog")
Dog("Bruno").eat()
# Bruno is eating
# Bruno is eating like a dog
The most common use is in __init__, so the parent sets up its own fields:
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Animal sets self.name
self.breed = breed
d = Dog("Bruno", "Labrador")
print(d.name, d.breed) # Bruno Labrador
Multi-level inheritance#
A child can itself be a parent, forming a chain:
class Animal:
def eat(self):
print("eating")
class Dog(Animal):
def bark(self):
print("barking")
class GoldenRetriever(Dog):
def fetch(self):
print("fetching")
g = GoldenRetriever()
g.eat() # from Animal
g.bark() # from Dog
g.fetch() # own
# lookup: GoldenRetriever -> Dog -> Animal -> object
Multiple inheritance#
A class can inherit from several parents:
class Vehicle:
def __init__(self, brand):
self.brand = brand
def start(self):
print(f"{self.brand} is starting")
class Electric:
def __init__(self, battery_capacity):
self.battery_capacity = battery_capacity
def charge(self):
print("Charging")
class Tesla(Electric, Vehicle):
def __init__(self, brand, battery_capacity):
# Parents have different __init__ signatures and don't call super(),
# so initialize each one explicitly
Vehicle.__init__(self, brand)
Electric.__init__(self, battery_capacity)
def autopilot(self):
print("Autopilot on")
t = Tesla("Tesla", 100)
t.start() # Tesla is starting
t.charge() # Charging
Here a single super().__init__(...) would only reach Electric (the next class in the MRO), because neither parent passes the call along. Explicit calls make sure both are initialized. If both parents define the same method, the one listed first wins, as the MRO shows.
MRO: method resolution order#
The MRO is the exact order Python searches classes for an attribute.
print(Tesla.__mro__)
# (Tesla, Electric, Vehicle, object)
Rules of thumb:
- Children come before parents.
- Parents are searched left to right, in the order you listed them.
- No class appears twice.
- It always ends with
object.
The diamond problem#
class A:
def show(self): print("A")
class B(A):
def show(self): print("B")
class C(A):
def show(self): print("C")
class D(B, C):
pass
D().show() # B
print(D.__mro__)
# (D, B, C, A, object)
Python resolves the diamond with C3 linearization. You don't need the algorithm; remember "children before parents, left to right, never repeat". Note that A comes after both B and C, so it is visited once.
super() vs an explicit parent call#
There are two ways to call a parent's method:
super() |
Parent.__init__(self, ...) |
|
|---|---|---|
| Use when | Single inheritance, or cooperative multiple inheritance where every class calls super() |
Parents with different signatures that don't cooperate |
| Follows the MRO? | Yes: calls the next class in the MRO | No: calls exactly that class |
Passes self? |
Automatically | You pass it |
Key takeaways#
- Inheritance reuses code by extending the attribute lookup chain to parent classes.
- Override to replace a method; use
super()to extend it. super()calls the next class in the MRO, which is not always the direct parent.- Multiple inheritance searches parents left to right; inspect
Class.__mro__when unsure. - The diamond problem is solved by C3 linearization: the shared base is visited once, last.