"Favor composition over inheritance" is one of those pieces of advice everyone repeats and almost no one demonstrates. So let me show you the exact moment inheritance breaks, with code, instead of just asserting that it does.
It always starts out clean
Say we are modelling game characters. Inheritance feels perfect at first:
class Character:
def move(self):
print("moving")
class Warrior(Character):
def attack(self):
print("swinging a sword")
class Mage(Character):
def cast(self):
print("casting a spell")Neat. A Warrior is a Character, a Mage is a Character. The hierarchy reads like a sentence. This is the version that makes it into every tutorial, and for a small, stable set of types it is genuinely fine.
Then the requirements arrive
Now the game designer wants a Battlemage who can both swing a sword and cast spells. With inheritance you have a few options, and all of them hurt.
You could inherit from both:
class Battlemage(Warrior, Mage):
passMultiple inheritance. It technically works in Python, but the method resolution order gets subtle fast, and the day two parents define the same method, you inherit a puzzle. In most languages this door isn't even open.
You could push attack and cast up into Character so everyone has them, but then your peaceful Merchant inherits an attack method it should never have. The base class becomes a junk drawer of abilities that only some subclasses want.
Or you could copy the methods into Battlemage and accept the duplication. Now a bug fix in attack has to be made in two places, and someone will forget one.
This is the wall. Inheritance forces every type into a single rigid tree, but real requirements are combinations - "can attack" and "can cast" are independent abilities that any character might mix and match. A tree can't express that.
Composition: build from capabilities, not ancestors
Instead of asking "what is this character," ask "what can it do." Model each ability as its own object and give a character the ones it needs.
class Attack:
def execute(self):
print("swinging a sword")
class Cast:
def execute(self):
print("casting a spell")
class Character:
def __init__(self, name, abilities=None):
self.name = name
self.abilities = abilities or {}
def use(self, ability):
action = self.abilities.get(ability)
if action is None:
print(f"{self.name} can't {ability}")
return
action.execute()Now every character is just a bundle of abilities, assembled however you like:
warrior = Character("Warrior", {"attack": Attack()})
mage = Character("Mage", {"cast": Cast()})
battlemage = Character("Battlemage", {"attack": Attack(), "cast": Cast()})
merchant = Character("Merchant") # no combat abilities, and that's fine
battlemage.use("attack") # swinging a sword
battlemage.use("cast") # casting a spellThe Battlemage that broke inheritance is now the easy case. It just has both abilities. The Merchant with no combat is easy too - it simply has none. No forced tree, no junk-drawer base class, no duplication.
Why "has-a" scales where "is-a" doesn't
Abilities combine freely. With N independent capabilities, inheritance would need a class for every combination you want. Composition needs N ability classes, and you mix them per character. The combinatorial explosion just disappears.
Behaviour is reusable in isolation. Attack lives on its own. Any character can hold it, and you can test it without constructing a character at all.
You can change abilities at runtime. A character can gain or lose an ability by adding or removing it from the dict. Inheritance is fixed the moment the object is created - you can't change an object's class mid-flight.
The base class stays small. Character only knows how to hold abilities and use them. It never accumulates methods that only some subclasses need.
This isn't "inheritance is bad"
Inheritance is the right tool when there genuinely is a stable "is-a" relationship and subclasses truly are specialisations that don't need to mix. A PermissionError is an OSError. That is real, it is stable, and modelling it as composition would be silly.
The trouble starts when you use inheritance to share code rather than to model a true is-a relationship, or when the things you are modelling are really combinations of independent traits. That is exactly when the hierarchy starts fighting you.
A quick gut check: if you ever find yourself wanting to inherit from two classes to get both of their behaviours, that is composition asking to be let in.
The takeaway
Inheritance models what something is; composition models what something has. The first is a rigid tree, the second is a flexible bag of parts. When your types are really combinations of independent abilities - and more often than not, they are - reach for composition. You will trade a tidy-looking diagram for code that actually bends when the requirements do.

