Welcome to the next lesson in our journey to becoming a backend engineer. In the previous sessions, we explored how to organize code using Python's functional features, culminating in creating custom decorators. We saw how decorators can wrap functions to add behavior like logging or timing, which is a powerful way to manage cross-cutting concerns.
Now, we shift our focus to another, more structural approach to organizing complex logic: Object-Oriented Programming (OOP). This lesson addresses a fundamental concept in OOP: how to model relationships between different types of objects using inheritance and composition. Your goal is to not only learn the syntax for each but, more importantly, to understand the design trade-offs and develop the intuition for when to use one over the other. Mastering this is a hallmark of an experienced developer and is critical for building the kind of scalable, maintainable systems required at a company like Infilect.
1. The Two Pillars of Code Reuse: Inheritance and Composition
In object-oriented programming, we rarely build classes that live in isolation. We build systems of interconnected objects that share features and behaviors. Inheritance and composition are the two primary mechanisms for establishing these relationships and reusing code.
- Inheritance models an "is-a" relationship. You use it when a new class is a more specialized version of an existing class. For example, a
Manageris anEmployee, but with additional responsibilities. - Composition models a "has-a" relationship. You use it when a class is built from or contains other objects. For example, an
Employeehas anAddress.
Let's begin by understanding the mechanics of inheritance.
2. Inheritance: Creating Specialized Classes (The "Is-A" Relationship)
Inheritance allows a new class, called a subclass or child class, to automatically acquire all the attributes and methods of an existing superclass or parent class. This is a powerful tool for code reuse when you have a clear hierarchical relationship.
The following video by Corey Schafer provides a clear, step-by-step walkthrough of how to implement inheritance in Python.
Python OOP Tutorial 4: Inheritance - Creating Subclasses
This video is a practical guide to creating subclasses in Python. It clearly demonstrates the core mechanics you need to know.
Please watch the following segments: Creating a basic subclass: This part shows how a Developer class can inherit from an Employee class and instantly gain all its functionality. Customizing a subclass: Here, you'll see how to override attributes (like raise_amount) in the subclass without affecting the parent. Extending the subclass: This is a crucial section. Pay close attention to how the __init__ method of the Developer class is extended to accept a new attribute (programming_language) and how super().__init__() is used to call the parent's constructor to handle the shared attributes.
As you saw, the key syntax is class Subclass(Superclass):. The super() function is the standard way to call methods on the parent class, ensuring that the parent's logic (like its __init__ method) is still executed.
Here is a quick summary of the structure:
class Employee:
def __init__(self, name, email):
self.name = name
self.email = email
def work(self):
print(f"{self.name} is working.")
# Developer "is-an" Employee
class Developer(Employee):
def __init__(self, name, email, prog_language):
# Let the parent class handle name and email
super().__init__(name, email)
# Add the new, specific attribute
self.prog_language = prog_language
def code(self):
print(f"{self.name} is writing code in {self.prog_language}.")
# A Developer object can use methods from both classes
dev = Developer("Ada Lovelace", "ada@example.com", "Python")
dev.work() # Inherited from Employee
dev.code() # Defined in Developer
Inheritance is effective, but it creates a very strong connection—or tight coupling—between the parent and child classes. Changes to the parent can easily break the child. This leads us to a more flexible alternative.
3. Composition: Building Complex Objects (The "Has-A" Relationship)
Composition is about creating complex objects by combining simpler, independent objects. Instead of inheriting from another class, an object simply holds an instance of that other class as one of its attributes. This creates a "has-a" relationship.
Let's see this in action by refactoring our HR system example. An Employee object needs a way to calculate payroll, but the payroll logic can be complex and vary. Instead of creating many subclasses for each payroll type (e.g., SalariedEmployee, HourlyEmployee), we can compose the Employee with a separate PayrollPolicy object.
The Real Python article "Inheritance and Composition: A Python OOP Guide" provides an excellent, detailed walkthrough of this concept. We'll start with a small piece that shows the core idea.
Inheritance and Composition: A Python OOP Guide
This article provides a deep dive into both concepts. We will focus on the section that redesigns a system using composition for greater flexibility.
Please read the subsection on flexible designs. As you read, focus on how the code is restructured: Notice the creation of separate, independent classes for different concerns: ProductivitySystem with Role classes, and PayrollSystem with Policy classes. Observe how the Employee class is simplified. Instead of inheriting from different employee types, a single Employee class is composed of role and payroll objects. It has a role and has a payroll policy.
This is the essence of composition:
# A self-contained, independent class for payroll logic
class HourlyPolicy:
def __init__(self, hours_worked, hour_rate):
self.hours_worked = hours_worked
self.hour_rate = hour_rate
def calculate_payroll(self):
return self.hours_worked * self.hour_rate
# The Employee class is composed with a payroll policy
class Employee:
def __init__(self, name, payroll_policy):
self.name = name
self.payroll = payroll_policy # The "has-a" relationship
def calculate_payroll(self):
# The Employee class delegates the calculation to its component
print(f"Calculating payroll for {self.name}...")
return self.payroll.calculate_payroll()
# Create a policy and compose it into an Employee
policy = HourlyPolicy(40, 50)
employee = Employee("John Doe", policy)
final_pay = employee.calculate_payroll() # Prints and returns 2000
The Employee class doesn't need to know the details of how payroll is calculated. It just needs to know that it has a payroll object that can do the job. This is called delegation. This design is loosely coupled and much more flexible. If we want to change John's pay structure to a salaried one, we can simply give his employee object a new payroll policy object at runtime, without changing the Employee class itself.
4. The Critical Choice: Inheritance vs. Composition
So, when should you use one over the other? While both enable code reuse, they solve different design problems. Choosing incorrectly can lead to rigid, fragile code. The prevailing wisdom in modern software design is to favor composition over inheritance.
This video from ArjanCodes provides an outstanding explanation of why this principle is so important. He takes an employee system, shows how an inheritance-based design leads to a "combinatorial explosion" of classes, and then refactors it to a clean, flexible design using composition.
Composition Is Better Than Inheritance in Python
This video powerfully illustrates the practical difference between inheritance- and composition-based designs, showing why composition is often superior.
Please watch these key sections: Introduction: The core distinction between "is-a" (inheritance) and "has-a" (composition) and the problem of coupling. The Inheritance Approach: Watch how trying to separate responsibilities (e.g., handling commissions) with inheritance quickly leads to duplicated code and an explosion of subclasses. This demonstrates the problem visually. The Composition Approach: See the refactoring. Notice how concerns are separated into independent classes (Commission, Contract) and then composed within a single Employee class. This is the key takeaway. Summary and Benefits: A recap of why the composition approach is more flexible and avoids the problems of the inheritance model.
The video makes the case clearly: composition leads to more flexible, scalable, and maintainable systems.

The end goal of a composition-based approach is a system like the one below, where a central Employee class is composed of different "policy" or "role" objects that define its behavior.

Hands-On Task: Design a Simple System
Now it's your turn to apply these concepts. Let's build a mini-system for managing different types of content in a CMS (Content Management System).
You will create three classes:
-
ContentClass (Parent Class):- This will be your base class.
- In its
__init__method, it should accepttitleandauthor. - It should have a method
display()that prints the title and author.
-
ArticleClass (Child Class - Inheritance):- An
Articleis a type ofContent. - It should inherit from the
Contentclass. - Its
__init__method should also accept abodytext attribute, in addition totitleandauthor. Remember to usesuper()to initialize the parent attributes. - Override the
display()method to also print thebodyof the article after printing the title and author.
- An
-
CommentClass (Component Class):- A
Commentis not a type ofContent; it's a separate entity that anArticlehas. - Its
__init__method should acceptcommenter_nameandtext. - It should have a method
display_comment()that prints the commenter's name and their text.
- A
-
Putting It Together (Composition):
- Modify your
Articleclass's__init__method to include a new attribute,self.comments, initialized as an empty list. This establishes the "has-a" relationship: anArticlehasComments. - Add a method to the
Articleclass calledadd_comment(commenter_name, text). This method should create a newCommentobject and append it to theself.commentslist. - Finally, update the
Article'sdisplay()method again. After printing the title, author, and body, it should loop throughself.commentsand calldisplay_comment()for each comment.
- Modify your
To test your code:
Create an Article instance. Add a couple of Comments to it using your add_comment method. Finally, call the display() method on your article object and verify that it prints the article details and all its comments.
Click here for a solution to check your work.
# The component class
class Comment:
def __init__(self, commenter_name, text):
self.commenter_name = commenter_name
self.text = text
def display_comment(self):
print(f" Comment by {self.commenter_name}: '{self.text}'")
# The parent class
class Content:
def __init__(self, title, author):
self.title = title
self.author = author
def display(self):
print(f"'{self.title}' by {self.author}")
# The child class using inheritance AND composition
class Article(Content):
def __init__(self, title, author, body):
# INHERITANCE: Initialize parent attributes
super().__init__(title, author)
self.body = body
# COMPOSITION: The article "has-a" list of comments
self.comments = []
def add_comment(self, commenter_name, text):
# Create a component object and add it to the list
comment = Comment(commenter_name, text)
self.comments.append(comment)
print(f"New comment added to '{self.title}'")
def display(self):
# Call parent's display method
super().display()
print("\n--- Body ---")
print(self.body)
print("\n--- Comments ---")
if not self.comments:
print("No comments yet.")
else:
# Delegate display to each comment object
for comment in self.comments:
comment.display_comment()
# --- Test the system ---
my_article = Article(
title="Favor Composition Over Inheritance",
author="Jane Doe",
body="This article explains why building objects from parts is often better than creating complex hierarchies."
)
my_article.add_comment("Bob", "Great explanation!")
my_article.add_comment("Alice", "This really cleared things up for me.")
print("\n" + "="*40)
my_article.display()
print("="*40)
Conclusion
In this lesson, you've explored the two fundamental ways to structure relationships between classes in Python. You've learned how to implement them and, crucially, started to build the intuition needed to choose between them.
Key Takeaways:
- Inheritance ("is-a"): Use for true specialization where a child class is a logical subtype of the parent. It creates a tight coupling between classes.
- Composition ("has-a"): Use for building complex objects from smaller, independent parts. This creates a loose coupling and is generally more flexible and easier to maintain.
- Guiding Principle: "Favor composition over inheritance." Start with composition as your default choice for code reuse and only use inheritance for clear "is-a" hierarchies. This mindset is key to writing scalable and robust backend code.
We've now established the foundational patterns for organizing our code into classes. In our next lesson, we will focus on making our programs more robust by implementing error handling with try...except blocks and setting up logging to track application events, both essential skills for any production system.