Introduction
Welcome to the next lesson in our journey to master the Pythonic mindset. In our previous session, we internalized the "batteries included" philosophy, learning to leverage powerful, pre-built components from the standard library like pathlib, collections, and itertools instead of reinventing the wheel.
Today, we extend that principle of using small, well-defined building blocks to the very structure of our own code. We will tackle one of the most significant shifts for a developer coming from a language like Java: learning to favor composition over inheritance.
This lesson directly addresses the learning outcome: Favor composition and small, composable objects over deep class hierarchies for flexible and maintainable code. We will dissect why the deep, rigid class hierarchies common in traditional OOP can be problematic in Python and how a compositional approach leads to more flexible, maintainable, and testable code.
The Core Problem: The "Is A" Trap and the Subclass Explosion
In object-oriented programming, inheritance creates an "is a" relationship. A Manager is an Employee. A SavingsAccount is an Account. This is a powerful modeling tool, but when used to specialize behavior along multiple, independent axes, it leads to what the "Gang of Four" (authors of the seminal book Design Patterns) called a "subclass explosion."
Imagine a payroll system. You might start with a base Employee class. Then you add subclasses for SalaryEmployee and HourlyEmployee. Now, some employees can earn a commission. Do you create SalaryEmployeeWithCommission and HourlyEmployeeWithCommission? What if they can also get a bonus? The number of required classes explodes combinatorially.
This image of a UML diagram illustrates a typical, inheritance-heavy design that can quickly become rigid and complex.

This rigid structure is a classic Java-style pattern. Let's read a concise explanation of this "subclass explosion" problem using a different example: a logging system.
The Composition Over Inheritance Principle - Problem: the subclass explosion
The Python Patterns guide provides an excellent, clear explanation of how specializing a class along two different axes (output destination and filtering) using inheritance leads to an unmanageable number of subclasses.
Read the section titled 'Problem: the subclass explosion'. Note how needing to combine 3 output types with 2 filtering modes would require 3 * 2 = 6 classes. This illustrates the core weakness of relying solely on inheritance for specialization.
The fundamental issue is that inheritance creates very tight coupling. A subclass is intrinsically tied to its parent's implementation details, making the system brittle and hard to change.
The Pythonic Solution: The "Has A" Relationship with Composition
Instead of an "is a" relationship, composition builds objects from other objects, creating a "has a" relationship. An Employee has a PaySchedule and has a CommissionPlan.
This approach treats behaviors as pluggable components. You can combine them at runtime, creating endless variations without a single new subclass. The result is a system that is more flexible, loosely coupled, and easier to test.
Let's see a masterclass in refactoring from an inheritance-based mess to a clean, compositional design.
Composition Is Better Than Inheritance in Python
ArjanCodes provides a perfect 'bad vs. good' walkthrough. First, he'll build an employee payment system using inheritance, demonstrating exactly how the 'subclass explosion' and code duplication happen in practice.
Watch the segment from 02:37 to 10:08. Pay close attention to how adding a 'commission' feature forces the creation of new subclasses like SalariedEmployeeWithCommission, leading to duplicated logic and a rigid design.
Now, watch the critical part where he dismantles the inheritance hierarchy and rebuilds it using composition. This is the essence of today's lesson.
Composition Is Better Than Inheritance in Python
Here is the Pythonic solution. He refactors the system by creating small, independent classes for each responsibility and then composing them inside the main Employee object.
Watch from 10:08 to 18:30. Notice these key steps: He creates separate classes for Commission and Contract (e.g., HourlyContract, SalariedContract). These are the small, composable objects. The Employee class no longer inherits these behaviors; it has them as attributes (employee.contract, employee.commission). This makes the system incredibly flexible. You can now create an employee with any combination of contract and commission by simply passing the right objects during instantiation.
This example makes the benefits tangible:
- Flexibility: You can mix and match components at runtime.
- Reduced Coupling:
Employeedoesn't need to know the internal details of how aContractorCommissionis calculated. It just callsget_payment(). - No Subclass Explosion: You have a few component classes, not a combinatorial number of employee subclasses.
- Single Responsibility Principle: Each class does one thing well. A
Contractclass only handles payment contracts. ACommissionclass only handles commissions.
Making Composition Work: The Role of Interfaces (Protocol)
For composition to work, the components need to agree on a common interface. In the video, both Contract and Commission objects had a get_payment() method. The Employee class could rely on this behavior being present.
As we touched on in a previous lesson, typing.Protocol is the ideal Pythonic tool for defining these behavioral contracts without forcing inheritance. This decouples our components even more than using traditional Abstract Base Classes (ABCs).
Let's examine a concrete example comparing an ABC-based approach with a Protocol-based one.
We need to talk about Protocols in Python
An article from Idego Group clearly lays out the problems with inheritance-based interfaces (ABCs) and shows how Protocols, which rely on duck typing, lead to a more decoupled design.
Read the sections from 'Abstract Base Class (ABC) example' through 'Further improvement'. Focus on: The problems with ABCs: They create strong coupling (class Dog(Animal):), require imports everywhere, and can force functions to accept overly broad interfaces. The Protocol solution: Classes don't need to inherit. They just need to have the right methods ('if it quacks like a duck...'). This reduces coupling. Interface Segregation: The best part is creating small, specific protocols for functions that only need a subset of behavior (e.g., a Feedable protocol with only a feed() method). This perfectly embodies the 'small, composable' philosophy.
Protocols are the glue that holds a compositional design together in a clean, decoupled way. You define the behavior you need, and any object that provides that behavior can be used, without any rigid inheritance requirement.
Common Anti-Patterns and Refactorings
Even in Python, the temptation to use inheritance-like patterns persists. One common pattern is the mixin. A mixin is a class that provides methods to other classes via multiple inheritance. While sometimes useful, it's still a form of inheritance and can lead to its own set of problems.
A true compositional approach is often better.
Avoid These BAD Practices in Python OOP
Let's look at another example from ArjanCodes where he refactors a design that uses mixins for logging and saving into a cleaner, compositional pattern.
Watch the segment on mixins from 17:24 to 21:00. He starts with LogMixin and SaveMixin using multiple inheritance. The key refactoring step is when he removes the inheritance and instead passes the logger and saver objects into the constructor. This pattern, known as Dependency Injection, is a pure and powerful form of composition.
Another anti-pattern is avoiding object-oriented design altogether and creating a "forest of if statements" inside a single monolithic class. While it might seem simpler at first, this approach is brittle and hard to maintain, test, and extend. A design built on small, composed objects is far superior for all the reasons outlined in this resource's analysis of the if-statement dodge. We won't read it now, but python-patterns.guide provides an excellent breakdown of why that approach fails in the long run.
Conclusion
Today we've dismantled a core tenet of classical OOP and replaced it with a more flexible, Pythonic principle. By shifting your thinking from "is a" (inheritance) to "has a" (composition), you can build systems that are easier to understand, maintain, and extend.
Key Takeaways:
- Favor Composition Over Inheritance: This is a fundamental principle of good Python design. Use it to build complex functionality from simple, independent parts.
- "Is A" vs. "Has A": Inheritance creates a rigid "is a" relationship that leads to strong coupling and subclass explosion. Composition creates a flexible "has a" relationship by combining objects at runtime.
- Small, Composable Objects: Design small classes or functions that do one thing well. These become the building blocks for your application.
- Protocols as Glue: Use
typing.Protocolto define the behavioral contracts between your components without forcing them into a rigid inheritance hierarchy. - Refactor Away from Inheritance: Be critical of deep inheritance hierarchies and mixins. Ask yourself if the relationship is truly "is a," or if it could be better modeled as "has a" by passing in a component.
In our next lesson, we'll dive into the heart of what makes Python's objects tick. We'll explore Python's Data Model, learning how implementing "dunder" methods like __repr__ and __iter__ allows your custom objects to integrate seamlessly with the language itself, making them feel truly native.