python inheritance lets one class build directly on top of another, inheriting its attributes and methods rather than duplicating them. This article covers all three shapes inheritance can take in Python — single, multilevel, and multiple inheritance python supports — along with the Method Resolution Order that determines exactly how Python resolves conflicts when multiple parent classes are involved.
Why Inheritance Exists
The problem it solves
Imagine writing separate Dog and Cat classes, both needing a name, an age, an eat() method, and a sleep() method. Without inheritance, you'd duplicate that shared logic in both classes — and every future change to that shared behavior would need to be made twice, in two separate places, with the ever-present risk of the two copies quietly drifting apart over time. Inheritance solves this by letting both Dog and Cat inherit shared attributes and behavior from a common Animal class, writing that logic exactly once.
Core terminology
The class being inherited from is called the parent, base, or superclass. The class doing the inheriting is called the child, derived class, or subclass. Inheritance models an "is-a" relationship — a Dog is an Animal. This is worth contrasting briefly with composition, a "has-a" relationship (a Car has an Engine, rather than being one) — a different way of structuring relationships between classes, covered in more depth in a later article, and often a better fit when "is-a" doesn't genuinely apply.
Basic syntax
class Animal:
pass
class Dog(Animal):
passclass Dog(Animal): tells Python that Dog inherits from Animal. A Dog instance automatically receives, for free, every attribute and method Animal defines — without Dog needing to redefine any of it.
Single Inheritance
One parent, one child
Single inheritance — one child class inheriting from exactly one parent — is the simplest, and by far the most common form.
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name} is eating")
def sleep(self):
print(f"{self.name} is sleeping")
class Dog(Animal):
def bark(self):
print(f"{self.name} says Woof!")
rex = Dog("Rex")
rex.eat() # Rex is eating — inherited from Animal
rex.sleep() # Rex is sleeping — inherited from Animal
rex.bark() # Rex says Woof! — defined directly on DogDog never defines eat() or sleep() itself — it inherits both directly from Animal, while adding its own specialized bark() method on top.
Method overriding
A child class can redefine a method it inherited, replacing the parent's version entirely for instances of that child:
class Animal:
def make_sound(self):
print("Some generic animal sound")
class Dog(Animal):
def make_sound(self): # overrides Animal's version
print("Woof!")
rex = Dog()
rex.make_sound() # Woof! — Dog's version, not Animal'sExtending rather than replacing, with super()
Sometimes you don't want to fully replace a parent's method — you want to extend it, adding some additional behavior while still running the original logic. super() lets you call the parent's version of a method from inside the overriding one:
class Animal:
def __init__(self, name):
self.name = name
def make_sound(self):
print("Some generic animal sound")
class Dog(Animal):
def make_sound(self):
super().make_sound() # still runs Animal's version first
print("Woof!") # then adds Dog's own behavior
rex = Dog("Rex")
rex.make_sound()
# Some generic animal sound
# Woof!This pattern — call the parent's version via super(), then add whatever additional behavior the subclass needs — is genuinely common, and it's exactly the same principle behind super().__init__(), covered in the earlier __init__ article, applied here to a regular method instead of the constructor specifically.
Multilevel Inheritance
A chain of inheritance
Multilevel inheritance is a chain: a grandchild class inherits from a child class, which itself inherits from a parent class — parent → child → grandchild.
A real-world framing
Think of it like a grandparent-father-child relationship. Each generation can add its own traits on top of what it inherited from the generation before it — the grandchild ends up with everything from both the parent and the child level, plus whatever it adds itself.
A practical example: Vehicle → Car → ElectricCar
class Vehicle:
def __init__(self, brand):
self.brand = brand
def honk(self):
print("Beep beep!")
class Car(Vehicle):
def __init__(self, brand, doors):
super().__init__(brand)
self.doors = doors
def drive(self):
print(f"Driving the {self.brand}")
class ElectricCar(Car):
def __init__(self, brand, doors, battery_range):
super().__init__(brand, doors)
self.battery_range = battery_range
def charge(self):
print(f"Charging — {self.battery_range} miles of range")
tesla = ElectricCar("Tesla", 4, 300)
tesla.honk() # Beep beep! — inherited from Vehicle
tesla.drive() # Driving the Tesla — inherited from Car
tesla.charge() # Charging — 300 miles of range — defined directly on ElectricCar
print(tesla.brand, tesla.doors, tesla.battery_range)
# Tesla 4 300Notice how each level's __init__ calls super().__init__(...), passing along whatever the level above it needs — ElectricCar calls Car's __init__, which in turn calls Vehicle's __init__. By the time ElectricCar.__init__ finishes, tesla has accumulated attributes from all three levels of the chain: brand (from Vehicle), doors (from Car), and battery_range (from ElectricCar itself).
Multiple Inheritance and the Diamond Problem
One child, several parents
Multiple inheritance lets a single child class inherit from more than one parent class simultaneously, gaining methods and attributes from all of them at once:
class Flyer:
def fly(self):
print("Flying!")
class Swimmer:
def swim(self):
print("Swimming!")
class Duck(Flyer, Swimmer):
pass
d Duck()
donald.fly() # Flying! — from Flyer
donald.swim() # Swimming! — from SwimmerDuck inherits from both Flyer and Swimmer at once, gaining both fly() and swim() without either parent knowing anything about the other.
The diamond problem
Multiple inheritance introduces a genuine ambiguity when two parent classes share a method name, or when they both share a common ancestor further up the hierarchy — a shape often drawn as a diamond:
Animal
/ \
Bird Reptile
\ /
Dinosaur (hypothetically!)
If both Bird and Reptile define their own version of some method, and Dinosaur inherits from both, which version should Dinosaur actually use when that method is called? This is the classic "diamond problem," and different languages resolve it differently — Python has a specific, well-defined answer, covered next.
How Python resolves it: MRO and C3 linearization
Python resolves this ambiguity using something called the Method Resolution Order (MRO) — a precise, deterministic search path computed using an algorithm called C3 linearization. In practical terms, the rule is: Python checks the child class first, then searches through the parent classes left to right, exactly in the order they're listed in the class definition.
class Bird:
def move(self):
print("Flying")
class Reptile:
def move(self):
print("Crawling")
class Dinosaur(Bird, Reptile):
pass
rex = Dinosaur()
rex.move() # Flying — Bird is listed first, so its version winsBecause Bird was listed before Reptile in class Dinosaur(Bird, Reptile):, Python's MRO checks Bird first and finds move() there — Reptile's version is never even reached for this particular call. Simply reversing the order in the class definition — class Dinosaur(Reptile, Bird): — would flip which version wins.
Understanding and Using MRO with super()
Inspecting the actual resolution order
You don't have to guess at the MRO — Python lets you inspect it directly, either through the __mro__ attribute or the .mro() method:
class Dinosaur(Bird, Reptile):
pass
print(Dinosaur.__mro__)
# (<class 'Dinosaur'>, <class 'Bird'>, <class 'Reptile'>, <class 'object'>)
print(Dinosaur.mro())
# same result, as a list instead of a tupleThis shows you the exact search path Python will follow when resolving any attribute or method on a Dinosaur instance: check Dinosaur itself first, then Bird, then Reptile, then finally object (the base class every Python class ultimately inherits from). Whenever you're unsure which version of a method will actually run in a multiple-inheritance hierarchy, checking __mro__ directly settles the question immediately, rather than reasoning it out by hand.
super() follows the MRO, not just "the parent"
This is a genuinely important and commonly misunderstood detail: in multiple inheritance, super() doesn't necessarily call "the parent class" in some simple, singular sense — it calls whatever comes next in the MRO chain, which can involve classes that aren't direct parents at all, depending on the hierarchy's shape.
This enables a pattern called cooperative multiple inheritance, where every class in the chain calls super() and lets Python's MRO ensure each class's method runs exactly once, in the correct order:
class Base:
def __init__(self):
print("Base init")
class A(Base):
def __init__(self):
super().__init__()
print("A init")
class B(Base):
def __init__(self):
super().__init__()
print("B init")
class C(A, B):
def __init__(self):
super().__init__()
print("C init")
c = C()
# Base init
# B init
# A init
# C initThis output might look surprising at first — B init printing before A init, even though A is listed first in class C(A, B):. This is exactly the cooperative pattern at work: each class's super().__init__() call doesn't jump straight to its own parent — it moves to whatever's next in C's full MRO chain, which correctly threads through A, then B, then Base, ensuring every class's __init__ runs exactly once, in a consistent, well-defined order — rather than Base.__init__ potentially running twice (once via A, once via B), which is exactly the kind of duplication cooperative multiple inheritance, powered by the MRO, is specifically designed to avoid.
Mixin classes
A common, genuinely practical use of multiple inheritance is the mixin pattern — small, focused parent classes designed specifically to be combined with others, each contributing one specific piece of reusable behavior:
class JSONSerializableMixin:
def to_json(self):
import json
return json.dumps(self.__dict__)
class LoggableMixin:
def log(self, message):
print(f"[{self.__class__.__name__}] {message}")
class User(JSONSerializableMixin, LoggableMixin):
def __init__(self, name, email):
self.name = name
self.email = email
user = User("Alex", "[email protected]")
user.log("User created") # [User] User created
print(user.to_json()) # {"name": "Alex", "email": "[email protected]"}Neither JSONSerializableMixin nor LoggableMixin is meant to be used on its own — each exists purely to be mixed into other classes, contributing one focused capability without needing to know anything about the classes it gets combined with.
A rule of thumb: keep hierarchies shallow
As a practical closing guideline: keep your inheritance hierarchies shallow — a handful of classes deep at most, whether single, multilevel, or multiple. The MRO handles arbitrarily complex hierarchies correctly and consistently, but humans reading and maintaining that code don't always find it easy to reason about, especially once multiple inheritance and several levels of depth combine together. If you find yourself needing to check __mro__ regularly just to understand which version of a method will actually run, that's a reasonable signal the hierarchy has grown more complex than it needs to be — and it may be worth reconsidering whether composition (the "has-a" relationship mentioned in Section 1) would express the same relationship more simply.