Python composition versus inheritance is one of the most important design decisions you make when building object-oriented systems, and the choice between them shapes how flexible, testable, and maintainable your code becomes over time. Inheritance, covered extensively in the previous articles of this section, creates an is-a relationship where a child class is a specialized version of its parent and automatically receives all of the parent's methods and attributes. Composition creates a has-a relationship where a class holds instances of other classes as attributes and delegates work to them through method calls. A Car class inheriting from a Vehicle class says a car is a vehicle, which is true. A Car class containing an Engine object as an attribute says a car has an engine, which is also true. The same real-world concept is modeled differently depending on which relationship you emphasize in code, and choosing the right one has consequences that ripple through every part of your system.
The software engineering community has developed a strong preference for composition over inheritance over the past few decades, and for good reasons. Inheritance creates tight coupling between the child and the parent. A change to the parent's implementation affects every child class, sometimes in surprising ways. Inheritance hierarchies tend to grow deeper over time as new requirements demand new specializations, and deep hierarchies are hard to understand because the behavior of a class at the bottom of the chain depends on every class above it. Composition avoids these problems by keeping classes independent and connected only through their public interfaces. A class that uses composition can swap the objects it contains at runtime, can combine behaviors from multiple sources without the complexity of multiple inheritance, and can change its internal implementation without affecting the classes it delegates to.
The articles on single inheritance and multiple inheritance covered the mechanics of building class hierarchies. This article addresses the question those articles deliberately left open: when should you use inheritance at all, and when is composition the better choice? The answer is not a simple rule but a framework for thinking about class relationships that helps you make the right decision for each specific design problem.
The is-a test for inheritance
The most reliable test for whether inheritance is appropriate is whether you can honestly say the child class is a kind of the parent class. A CheckingAccount is a kind of BankAccount. A Cat is a kind of Animal. A Square is a kind of Shape. In each case, the child shares the parent's fundamental nature and can be used anywhere the parent is expected. Any code that works with a BankAccount should work with a CheckingAccount without modification. This is the Liskov substitution principle applied to class design: the child must be usable through the parent's interface without surprising the caller.
When the is-a relationship is genuine, inheritance provides the right level of code reuse. The child inherits the parent's public interface and most of its implementation, overriding only the methods that need specialization. The parent defines what all accounts, animals, or shapes have in common, and each child adds what makes it distinct. This is inheritance at its best: a small number of well-understood levels where each level adds meaningful specialization.
The is-a test also helps you recognize when inheritance is the wrong choice. A Car is not a kind of Engine, even though a car uses an engine. A Stack is not a kind of List, even though a stack could be implemented using a list internally. In both cases, the relationship is has-a, not is-a, and inheritance would create a class that inherits methods it should not expose. A Car that inherits from Engine would have a start method, which is correct, but also a compression_ratio attribute, which is an implementation detail of engines that car users should never see. A Stack that inherits from List would expose insert and remove methods that violate the stack's last-in-first-out contract.
Composition through delegation
Composition is implemented by storing an instance of one class as an attribute of another and delegating method calls to that stored instance. The containing class does not inherit any methods from the contained class. It explicitly calls the methods it needs, which gives it complete control over which behaviors are exposed and how they are named. This explicit delegation is more typing than inheritance, where methods are inherited automatically, but the control it provides is worth the extra keystrokes in any non-trivial system.
Here is a Stack class implemented through composition with a list rather than through inheritance:
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
if not self._items:
raise IndexError("pop from empty stack")
return self._items.pop()The core push and pop methods provide the last-in-first-out behavior. Peek lets callers inspect the top element without removing it, and the length method integrates with Python's built-in len function:
def peek(self):
if not self._items:
raise IndexError("peek at empty stack")
return self._items[-1]
def __len__(self):
return len(self._items)The Stack class contains a list as its internal storage but exposes only the operations that make sense for a stack: push, pop, peek, and length. The list's insert, remove, sort, and reverse methods are not accessible through the Stack's public interface, which preserves the stack's contract. If the internal storage needs to change from a list to a linked list for performance reasons, the change is confined to the Stack class and no code that uses Stack objects needs to change. This is the encapsulation benefit of composition: the containing class owns its internal representation completely.
Swapping behavior at runtime with composition
One of composition's greatest strengths is the ability to change an object's behavior at runtime by swapping the objects it delegates to. This pattern, sometimes called the strategy pattern, lets you configure an object's behavior after creation without changing its class. Inheritance cannot do this because the parent class is fixed at definition time. A child class inherits from one parent and that relationship cannot be changed when the program is running.
Consider a report generator that can output in different formats. With composition, the formatter is a separate object:
class PDFFormatter:
def format(self, data):
return f"[PDF] {data}"
class HTMLFormatter:
def format(self, data):
return f"<html><body>{data}</body></html>"A Report object is created with a formatter, and the formatter can be replaced later by assigning a new object to the formatter attribute:
class Report:
def __init__(self, formatter):
self.formatter = formatter
self.data = ""
def generate(self):
return self.formatter.format(self.data)The Report class does not inherit from PDFFormatter or HTMLFormatter because a report is not a kind of formatter. A report uses a formatter, which is a has-a relationship modeled through composition. If a third formatter type is added, the Report class works with it without modification.
Making the right choice for your design
The choice between composition and inheritance is not binary. Most well-designed classes use both. A class might inherit from a parent to establish its fundamental identity while using composition to acquire additional capabilities. A Manager class might inherit from Employee because a manager is fundamentally an employee, and it might contain a list of Employee objects because a manager has direct reports. The inheritance captures the is-a relationship, and the composition captures the has-a relationship. Both are correct for what they model.
When you are unsure which to choose, start with composition. It is easier to convert composition to inheritance later if you discover a genuine is-a relationship than it is to untangle an inheritance hierarchy that has grown too deep. Composition also makes your classes easier to test because you can pass mock objects as the contained instances, isolating the class under test from its dependencies. Inheritance makes testing harder because the child and parent are tightly coupled, and testing the child often requires setting up the parent's full state.
The article on class relationships in Python explores the full taxonomy of how classes can relate to each other, including association, aggregation, and dependency, providing a complete framework for thinking about class design beyond the composition versus inheritance binary.
Rune AI
Key Insights
- Inheritance creates an is-a relationship; composition creates a has-a relationship where one object contains and delegates to another.
- Composition is more flexible than inheritance because contained objects can be changed at runtime and implementations can be swapped easily.
- Favor composition when the relationship is not clearly hierarchical; use inheritance when the child genuinely specializes the parent.
- Composition avoids the tight coupling that deep inheritance chains create, making code easier to test and change.
- Most real-world classes use both patterns: inheritance for fundamental identity and composition for acquired capabilities.
Frequently Asked Questions
What is the difference between composition and inheritance?
Why is composition often recommended over inheritance?
Can I use both composition and inheritance in the same class?
Conclusion
Composition and inheritance are both essential tools in object-oriented design, and the skill is knowing which one fits the problem at hand. Inheritance works best for clear is-a relationships where the child is fundamentally a specialized version of the parent and shares most of the parent's interface. Composition works best for has-a relationships where an object uses another object to get work done, and it offers the flexibility to change that helper object at runtime or swap implementations without affecting the containing class.
More in this topic
Create Custom Iterators in Python
Learn how to build your own iterators in Python by implementing the __iter__() and __next__() methods, with practical examples and reusable patterns.
Python Iterator Protocol Explained
Learn how Python's iterator protocol works with the __iter__() and __next__() methods, and understand the contract every iterator must follow.
Python Iterables and Iterators Explained
Learn what iterables and iterators are in Python, how they power for loops under the hood, and why understanding them unlocks cleaner data processing patterns.