Python Inheritance vs Composition: Choosing the Right Design
python inheritance vs composition: Compare Python inheritance and composition with code examples, tradeoffs, and guidance for choosing the right approach in your design.
When you design a Python class hierarchy, the choice between python inheritance vs composition determines how tightly coupled your objects are and how easily you can change behavior later. Inheritance builds a new class from an existing one, while composition builds a class by containing instances of other classes. Both reuse code, but they create different relationships between objects, and those differences affect maintainability, testing, and flexibility.
The Core Difference Between Inheritance and Composition in Python
Inheritance creates an "is-a" relationship. A subclass inherits attributes and methods from a parent class, and you can override them to specialize behavior. Composition creates a "has-a" relationship. A class holds references to other objects and delegates work to them.
The practical difference shows up when you need to change behavior. With inheritance, changing a parent class affects all subclasses. With composition, you can swap the contained object at runtime, as long as it satisfies the expected interface.
Consider a simple example. A Dog class might inherit from Animal:
class Animal: def speak(self): return "..." class Dog(Animal): def speak(self): return "Woof"
The same behavior via composition looks like this:
class Dog: def __init__(self, sound_behavior): self.sound_behavior = sound_behavior def speak(self): return self.sound_behavior.make_sound()
Here Dog does not know how to make a sound; it delegates to a sound_behavior object. That object can be swapped, mocked, or changed without touching Dog.
Inheritance in Python: Syntax and Behavior
Python's inheritance syntax is straightforward. You list parent classes in parentheses after the class name. The child class gets all attributes and methods from the parent, unless it overrides them.
class Vehicle: def __init__(self, make, model): self.make = make self.model = model def description(self): return f"{self.make} {self.model}" class Car(Vehicle): def __init__(self, make, model, doors): super().__init__(make, model) self.doors = doors
super() lets you call the parent's __init__ and other methods. Python uses the method resolution order (MRO) to find the next method in the hierarchy. For single inheritance, the MRO is simple: child, then parent, then object.
Inheritance is useful when you have a clear type hierarchy and subclasses genuinely extend the parent's behavior. For example, a SavingsAccount is a specific kind of BankAccount. The parent can hold common logic like deposit and withdrawal, and the child adds interest calculations.
But inheritance has a cost. The child is permanently tied to the parent's implementation. A change in the parent's method signature or internal behavior can break subclasses. This coupling becomes more severe as the hierarchy grows.
Composition in Python: Building Objects from Parts
Composition means a class contains other objects as attributes and uses them to perform its responsibilities. The contained objects are often injected through the constructor, which makes the relationship explicit and testable.
class Engine: def start(self): return "Engine started" class Car: def __init__(self, engine): self.engine = engine def start(self): return self.engine.start()
Here Car does not inherit from Engine; it holds an Engine instance. You can pass any object that has a start method, not necessarily an Engine. This is often called "duck typing" in Python.
Composition gives you more control over the lifecycle of the contained object. You can create it lazily, share it between multiple owners, or replace it during the object's lifetime. This is especially useful when the behavior varies by configuration or context.
For example, a User class might compose a Notifier that can be an EmailNotifier or an SMSNotifier. The User does not care which one it receives; it just calls notify(message).
class User: def __init__(self, name, notifier): self.name = name self.notifier = notifier def notify(self, message): self.notifier.send(self.name, message)
The notifier can be swapped at runtime, which is impossible with inheritance unless you change the class itself.
Why Multiple Inheritance Pushes Developers Toward Composition
Python supports multiple inheritance, but it introduces complexity. The MRO determines which parent method is called when a class inherits from multiple parents. The diamond problem appears when two parents share a common ancestor, and the MRO must decide the order.
class A: def method(self): return "A" class B(A): def method(self): return "B" class C(A): def method(self): return "C" class D(B, C): pass
In this case, D().method() returns "B" because the MRO is D -> B -> C -> A. This is often surprising. The order depends on the class declaration, and changing it can change behavior in subtle ways.
Multiple inheritance also makes it harder to reason about which parent's __init__ runs and how super() calls chain. You must coordinate super() calls across all parents, which is error-prone.
Composition avoids these problems entirely. Instead of inheriting from multiple classes, you hold instances of those classes as attributes. Each contained object manages its own behavior independently. There is no MRO to debug, and you do not have to worry about method name collisions across parents.
Choosing Between Inheritance and Composition for a Given Design
The decision depends on the nature of the relationship. Use inheritance when the child is a true subtype of the parent and you want to reuse the parent's interface. For example, a Circle is a Shape and should inherit area and perimeter methods.
Use composition when you need to swap behavior, when the relationship is "has-a" rather than "is-a", or when you want to avoid deep hierarchies. A Report class might compose a Formatter and a DataSource because the report does not become a formatter or a data source; it uses them.
Here are concrete criteria:
- If you need to override a method to change its implementation, inheritance is natural.
- If you need to change behavior at runtime, composition is required.
- If you want to reuse a large set of methods without modification, inheritance saves boilerplate.
- If you want to keep classes small and focused, composition encourages that.
A common pattern is to use inheritance for a stable base and composition for varying parts. For example, a PaymentProcessor base class can define the skeleton of the payment flow, while the actual payment gateway is composed in as a separate object.
How Composition Affects Testing and Maintainability
Composition makes testing easier because you can inject mock objects. In the User example, you can pass a fake notifier that records messages instead of sending them. With inheritance, you would need to mock the parent class or use complex patching.
class FakeNotifier: def __init__(self): self.messages = [] def send(self, name, message): self.messages.append((name, message)) def test_user_notify(): fake = FakeNotifier() user = User("Alice", fake) user.notify("Hello") assert fake.messages == [("Alice", "Hello")]
This test does not touch any external service. It verifies that User delegates correctly to the notifier. With inheritance, you would have to subclass User to override the notifier, which is more invasive.
Maintainability also improves because composition reduces coupling. A change to the contained object's implementation does not affect the containing class as long as the interface stays the same. Inheritance, on the other hand, creates a compile-time (or runtime) dependency that can ripple through the hierarchy.
Composition also avoids the "fragile base class" problem, where a change to a base class breaks subclasses that depend on its internal details. When you compose, you explicitly define what you need from the contained object, usually through its public methods.
Performance and Runtime Considerations
Performance differences between inheritance and composition are usually negligible in Python. Attribute lookup in inheritance follows the MRO, which is a fixed sequence. Composition adds one extra attribute access because you first get the contained object and then call its method.
For example, self.engine.start() requires two attribute lookups: self.engine and then start. In inheritance, self.start() is a single lookup. In tight loops or high-frequency calls, this can add a small overhead, but it is rarely the bottleneck in real applications.
A more significant consideration is memory. Composition can use more memory if you create many small objects, but this is also rarely a deciding factor. The bigger cost is often the complexity of managing object lifetimes and dependencies.
If you are building a library where performance is critical, you might choose inheritance to avoid the extra indirection. But for most business logic, the maintainability benefits of composition outweigh the micro-performance cost.
One area where composition has a clear advantage is when you need to share a single instance across multiple objects. With composition, you can pass the same engine to several cars. With inheritance, each car would have its own copy of the engine's state, which is not what you want.
Composition also works better with Python's dynamic features. You can replace a contained object at runtime, or use a proxy that delegates to different implementations based on state. This is difficult to achieve with inheritance without changing the class itself.
In practice, many Python projects use a mix. A base class defines the core interface, and composition provides pluggable behavior. The standard library's collections.abc classes use inheritance to define abstract interfaces, while concrete implementations often compose internal data structures.
When you face the python inheritance vs composition decision, think about the relationship's stability. If the subtype relationship is fixed and unlikely to change, inheritance is appropriate. If you anticipate changes in behavior, or if you want to test components in isolation, composition is the safer choice.