On this page
Inheritance and composition in Python are the two ways classes relate to each other. Inheritance makes one class a specialised version of another, while composition builds a class from other objects it holds. Prefer composition by default and use inheritance for genuine "is a" relationships kept shallow.
The syntax is the easy part. The design mistakes cost more: naming the parent class instead of calling super() can silently skip a class under multiple inheritance, and a five-level hierarchy makes it hard to find which class a method comes from. This part is for readers who know Python classes and objects from Part 1. It covers super() and the MRO, composition, duck typing, class and static methods and abstract base classes, with a table for choosing between them.
How does inheritance work in Python?
A subclass gets the parent's attributes and methods, and can add or override them:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def describe(self):
return f"{self.name} earns {self.salary:,}"
def annual_cost(self):
return self.salary * 12
class Manager(Employee):
def __init__(self, name, salary, reports):
super().__init__(name, salary) # run the parent's setup
self.reports = reports
def describe(self): # override
base = super().describe()
return f"{base} and manages {len(self.reports)} people"
m = Manager("Rekha", 180_000, ["Amit", "Sara"])
print(m.describe()) # Rekha earns 180,000 and manages 2 people
print(m.annual_cost()) # 2160000 - inherited unchanged
Two things to note. First, super().__init__(...) runs the parent initialiser rather than duplicating it. Second, super().describe() inside the override lets you extend behaviour instead of replacing it.
Forget the super().__init__ call and the parent's attributes never get set. Accessing m.name would then raise AttributeError: 'Manager' object has no attribute 'name'.
Why use super() instead of naming the parent?
Always use super() rather than Employee.__init__(self, ...). The super() documentation describes it as returning a proxy that delegates to "a parent or sibling class", following the method resolution order (MRO). Under multiple inheritance, the next class is not necessarily the one you would have named:
class Base:
def greet(self):
return "Base"
class Logged(Base):
def greet(self):
return "Logged > " + super().greet()
class Cached(Base):
def greet(self):
return "Cached > " + super().greet()
class Service(Logged, Cached):
def greet(self):
return "Service > " + super().greet()
print(Service().greet())
# Service > Logged > Cached > Base
print([k.__name__ for k in Service.__mro__])
# ['Service', 'Logged', 'Cached', 'Base', 'object']
Inside Logged, super() points to Cached, a sibling, not to Base. Consequently every class runs exactly once. Had Logged called Base.greet(self) directly, Cached would have been skipped. The tutorial section on multiple inheritance explains how the order is computed.
Why does composition usually win over inheritance?
Inheritance couples classes tightly. The subclass depends on the parent's internals, and changing the parent can break children in non-obvious ways. This is known as the "fragile base class" problem.
Composition holds a reference instead:
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
return "Engine started"
class ElectricMotor:
def __init__(self, kilowatts):
self.kilowatts = kilowatts
def start(self):
return "Motor humming"
class Car:
def __init__(self, model, power_unit):
self.model = model
self.power_unit = power_unit # has-a, not is-a
def start(self):
return f"{self.model}: {self.power_unit.start()}"
print(Car("Nexon", Engine(120)).start()) # Nexon: Engine started
print(Car("Nexon EV", ElectricMotor(95)).start()) # Nexon EV: Motor humming
The power unit is swappable at runtime, and Car does not depend on either class's internals. Testing is also easy, because you can pass a fake object with a start method.
The heuristic: if you would say "is a", consider inheritance. If you would say "has a", use composition. Most real relationships are "has a".
Inheritance and composition in Python compared
| Question | Inheritance | Composition | Duck typing |
|---|---|---|---|
| Relationship | "is a" | "has a" | "behaves like" |
| Coupling | Tight: subclass sees parent internals | Loose: talks through methods | None: only the method name matters |
| Swap behaviour at runtime | No | Yes | Yes |
| Code reuse | Automatic | Explicit delegation | None |
| Typical Python use | Custom exceptions, framework base classes | Services, pipelines, models with parts | Exporters, file-like objects, plugins |
How does duck typing remove the need for inheritance?
Python does not require a shared base class for polymorphism. If two objects have the same method, they are interchangeable:
import json
class CSVExporter:
def export(self, rows):
return "\n".join(",".join(map(str, r)) for r in rows)
class JSONExporter:
def export(self, rows):
return json.dumps(rows)
def save(exporter, rows):
return exporter.export(rows) # neither class inherits from anything
rows = [[1, "a"], [2, "b"]]
print(save(CSVExporter(), rows)) # 1,a then 2,b on the next line
print(save(JSONExporter(), rows)) # [[1, "a"], [2, "b"]]
In a statically typed language this would need an interface. In Python, having the method is enough, which removes a large share of the inheritance you might otherwise write. If you want a type checker to verify the shape, typing.Protocol describes it without forcing the classes to inherit anything.
What is the difference between a class method and a static method?
A classmethod receives the class as cls and suits alternative constructors; a staticmethod receives nothing and is a plain function kept in the class:
class Date:
def __init__(self, day, month, year):
self.day, self.month, self.year = day, month, year
@classmethod
def from_string(cls, text):
d, m, y = map(int, text.split("-"))
return cls(d, m, y) # cls, so subclasses get their own type
@staticmethod
def is_leap(year):
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
d = Date.from_string("15-08-2026")
print(d.day, d.month, d.year) # 15 8 2026
print(Date.is_leap(2028)) # True
print(Date.is_leap(2100)) # False
class IndianDate(Date):
pass
print(type(IndianDate.from_string("15-08-2026")).__name__) # IndianDate
A classmethod receives the class and is the idiomatic way to write alternative constructors. Using cls(...) rather than Date(...) means a subclass calling from_string gets an instance of itself, as the last line shows.
By contrast, a staticmethod receives nothing automatically. It is a plain function that lives in the class for organisational reasons.
When are abstract base classes worth it?
Use one when you want to define an interface and fail loudly if it is not implemented:
from abc import ABC, abstractmethod
class Model(ABC):
@abstractmethod
def fit(self, X, y): ...
@abstractmethod
def predict(self, X): ...
def score(self, X, y): # concrete, shared by all subclasses
preds = self.predict(X)
return sum(p == t for p, t in zip(preds, y)) / len(y)
class Baseline(Model):
def fit(self, X, y):
self.majority = max(set(y), key=list(y).count)
return self
def predict(self, X):
return [self.majority] * len(X)
X = [[0], [1], [2], [3]]
y = ["spam", "ham", "ham", "ham"]
print(Baseline().fit(X, y).score(X, y)) # 0.75
Model()
# TypeError: Can't instantiate abstract class Model without an
# implementation for abstract methods 'fit', 'predict'
As the abc module documentation states, a class with abstract methods cannot be instantiated until all of them are overridden. A subclass missing predict therefore fails at instantiation rather than deep inside a call stack later, which is the whole point. The exact wording of the error message varies slightly between Python versions.
Common mistakes with inheritance
- Forgetting
super().__init__(). The parent's attributes are never set. - Hardcoding the parent class.
Employee.__init__(self, ...)breaks cooperative multiple inheritance. - Inheriting to reuse one helper method. Move the helper into a function or a component instead.
- Returning
Date(...)from a classmethod. Subclasses then get the wrong type; returncls(...). - Deep hierarchies. Five levels of subclasses make it hard to find which class implements a method.
How deep should an inheritance hierarchy go?
Two levels is usually plenty. The most common OOP failure in real codebases is not wrong syntax. Instead, it is a five-level inheritance chain where finding which class actually implements a method requires reading all five.
If you need more, that is a strong signal that some of those relationships are "has a" and want composition instead.
Practical guidance: prefer functions to classes, prefer composition to inheritance, prefer shallow to deep, and only add abstraction when the duplication actually hurts.
The Sunday Growth Brief
One email a week: the best new comparisons, a fresh roadmap and the tech news worth your attention.
No spam. Unsubscribe in one click.
Related reading
Python classes and objects (OOP Part 1) covers classes, instances and dunder methods. Custom error hierarchies are a good place to practise inheritance, as shown in exception handling in Python. For functional patterns, see lambda functions and iterators and generators.
Frequently asked questions
What is the difference between inheritance and composition?
Inheritance says a Manager is an Employee and gets its behaviour automatically. Composition says a Car has an Engine and delegates to it. Composition is usually more flexible, because you can swap the component at runtime and the two classes do not depend on each other's internals, which also makes testing with fakes easy.
What does super() actually do?
super() returns a proxy object that delegates method calls to the next class in the method resolution order. Under multiple inheritance that next class can be a sibling rather than the direct parent. That is precisely why you should use super() instead of hardcoding ParentClass.method(self), which skips classes and breaks cooperative designs.
What is the difference between a class method and a static method?
A classmethod receives the class itself as its first argument, usually named cls, and is typically used for alternative constructors such as from_string. A staticmethod receives nothing automatically. It is really just a plain function grouped inside the class for organisation, and it cannot see the class or instance unless you pass them.
Do I need abstract base classes?
Rarely in small codebases. They earn their place when you are defining an interface that several people or plugins will implement, and you want a clear TypeError at instantiation rather than an AttributeError deep in a call stack. For lighter-weight checking with type checkers, typing.Protocol describes the same interface without inheritance.
Written by
Althaf Ashraf
AI Systems Engineer, Tata Consultancy Services
AI systems engineer working on agentic decision systems and retrieval architectures at TCS, with a focus on getting AI into real workflows rather than demos.

