Sitemap
Marcus T

Marcus T

Software dev

Composition vs Inheritance in Python

4 min readMay 5, 2026

--

Press enter or click to view image in full size

If you read the previous article on abstract classes, you’ve already started thinking in terms of structure and contracts. Now we’re going one level deeper into design decisions — specifically:

How should objects relate to each other?

Two of the most common answers are:

  • Inheritance (the classic is-a relationship)
  • Composition (the flexible has-a relationship)

Both are powerful.
Both are useful.
Both can quietly make your code either elegant… or painful to maintain.

Let’s unpack them with real examples and practical intuition.

Inheritance feels like the right answer at the start. You see a few related things — cars, trucks, motorcycles — and the traditional education tells you to make a Vehicle base class and let everything else inherit from it. Clean. Object-oriented. Satisfying.

Then someone asks for an electric flying car, and your beautiful hierarchy collapses into a pile of override hacks and super() calls that nobody understands six months later.

The Fragile Hierarchy

Inheritance models a hierarchy. Here is a Vehicle hierarchy that looks reasonable on day one:

class Vehicle:
def __init__(self, name, max_speed):
self.name = name
self.max_speed = max_speed
def start(self):
print(f"{self.name} engine started.")
def move(self):
print(f"{self.name} is moving at {self.max_speed} mph.")

class Car(Vehicle):
def __init__(self, name, max_speed, num_wheels=4):
super().__init__(name, max_speed)
self.num_wheels = num_wheels
def drive(self):
print(f"{self.name} is driving on {self.num_wheels} wheels.")

class Truck(Car):
def __init__(self, name, max_speed, cargo_capacity):
super().__init__(name, max_speed)
self.cargo_capacity = cargo_capacity
def haul(self):
print(f"{self.name} is hauling {self.cargo_capacity} tons.")

This works just fine until it doesn’t. Product comes along and says, “We need an electric car.”

It seems like an easy update to define ElectricCar(Car). Override start() to be silent, add a battery_level attribute. Not as clean as the initial hierarchy, but manageable.

Then: “We need a flying car.”

Now you’re stuck. Does FlyingCar inherit from Car? It has wheels, but it also flies. Do you add a fly() method to Vehicle so every car and truck pretends it can fly and raises NotImplementedError? Do you make a parallel Aircraft hierarchy and have FlyingCar inherit from both?

class Aircraft(Vehicle):
def fly(self):
print(f"{self.name} is flying.")

class FlyingCar(Car, Aircraft): # multiple inheritance, here we go
pass

This compiles and runs. But now you’ve introduced the diamond problem: FlyingCar inherits from both Car and Aircraft, which both inherit from Vehicle. When you call start(), which path does Python take? Python resolves it with the MRO (Method Resolution Order, viewable via FlyingCar.__mro__), but the resolution is implicit and depends on the order you list parents. Change class FlyingCar(Aircraft, Car) and the behavior shifts subtly.

Now add: “electric flying car.” Do you make ElectricFlyingCar(ElectricCar, FlyingCar)? You've got four-way inheritance, two diamond intersections, and a super().__init__() chain that you have to draw on a whiteboard to debug. The hierarchy is no longer modeling vehicles — it's modeling every possible combination of vehicle features, and that combinatorial explosion is what kills inheritance-heavy designs.

Get Marcus T’s stories in your inbox

Join Medium for free to get updates from this writer.

The real problem is deeper than syntax. Inheritance forced us to commit, up front, to a single axis of classification (what kind of vehicle is it?). Real vehicles vary along several independent axes: how they’re powered, how they move, what they carry. The tight coupling of inheritance can model one axis well. It cannot model many.

The Same Problem, Composed

Composition builds objects from smaller, reusable pieces. Instead of asking “what is this thing?” we ask “what can this thing do, and what does it have?” Each capability becomes its own small class, and a vehicle is assembled by injecting the capabilities it needs.

# Capabilities — each one does exactly one thing.
class GasEngine:
def start(self, name):
print(f"{name} gas engine started with a vroom vroom.")

class ElectricMotor:
def start(self, name):
print(f"{name} electric motor whirs to life silently.")

class WheeledMovement:
def __init__(self, num_wheels=4):
self.num_wheels = num_wheels
def move(self, name):
print(f"{name} drives on {self.num_wheels} wheels.")

class FlightMovement:
def move(self, name):
print(f"{name} takes off and flies.")

class CargoHold:
def __init__(self, capacity, units):
self.capacity = capacity
self.units = units
def haul(self, name):
print(f"{name} hauls {self.capacity} {self.units}.")

# The vehicle itself is just a coordinator.
class Vehicle:
def __init__(self, name, max_speed, power_source, movement, cargo=None):
self.name = name
self.max_speed = max_speed
self.power_source = power_source
self.movement = movement
self.cargo = cargo
def start(self):
self.power_source.start(self.name)
def move(self):
self.movement.move(self.name)
def haul(self):
if self.cargo:
self.cargo.haul(self.name)
else:
print(f"{self.name} has no cargo hold.")

Vehicle doesn’t inherit from GasEngine or ElectricMotor. It delegates behavior. Now building any vehicle is much easier.

sedan         = Vehicle("Sedan", 120, GasEngine(), WheeledMovement(4))
electric_car = Vehicle("Tesla", 150, ElectricMotor(), WheeledMovement(4))
truck = Vehicle("Truck", 90, GasEngine(), WheeledMovement(6), CargoHold(10))
flying_car = Vehicle("AeroMobil", 200, GasEngine(), FlightMovement())
electro_flyer = Vehicle("Skydrive", 180, ElectricMotor(), FlightMovement())

When inheritance is still the right call

Favoring composition over inheritance is heavily emphasized in books like Design Patterns: Elements of Reusable Object-Oriented Software. But don’t think that you should never use inheritance. Rather, use inheritance carefully as it is expensive to change.

Inheritance is the right tool when:

  • The relationship is genuinely is-a and stable. A SavingsAccount truly is a kind of BankAccount. A PermissionError truly is an OSError. These relationships don't shift when requirements change.
  • You’re extending a framework that expects subclassing. Django models, Python’s Exception hierarchy, Pytest fixtures — these are designed around inheritance, and fighting that is silly.
  • The hierarchy is shallow (one or two levels) and the parent class is abstract or stable. Most well-designed inheritance trees stop at depth two.

The trouble starts when you reach for inheritance to share behavior between things that aren’t really the same kind of thing — that’s when composition is almost always better.

When to Use Composition

Use composition when:

  • Behavior may change at runtime
  • You want to mix and match features
  • You want to avoid deep hierarchies

Think:

  • Strategy pattern
  • Plugin systems
  • Configurable components

Final Thought

Inheritance gives you structure.
Composition gives you freedom.

Most beginners overuse inheritance because it feels intuitive. Experienced developers lean toward composition because they’ve experienced the pain of using inheritance before.

--

--