Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 15

Structuring Backend Logic with Inheritance and Composition

Recommended resources

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 Manager is an Employee, 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 Employee has an Address.

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.

This table provides a concise summary of the key differences between inheritance and composition.

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.

This UML diagram illustrates a policy-based design using composition. The central `Employee` class is composed of an `Address` object and delegates behavior to objects that implement the `IRole` and `IPayrollCalculator` interfaces. This is a very flexible and common pattern in backend development.

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:

  1. Content Class (Parent Class):

    • This will be your base class.
    • In its __init__ method, it should accept title and author.
    • It should have a method display() that prints the title and author.
  2. Article Class (Child Class - Inheritance):

    • An Article is a type of Content.
    • It should inherit from the Content class.
    • Its __init__ method should also accept a body text attribute, in addition to title and author. Remember to use super() to initialize the parent attributes.
    • Override the display() method to also print the body of the article after printing the title and author.
  3. Comment Class (Component Class):

    • A Comment is not a type of Content; it's a separate entity that an Article has.
    • Its __init__ method should accept commenter_name and text.
    • It should have a method display_comment() that prints the commenter's name and their text.
  4. Putting It Together (Composition):

    • Modify your Article class's __init__ method to include a new attribute, self.comments, initialized as an empty list. This establishes the "has-a" relationship: an Article has Comments.
    • Add a method to the Article class called add_comment(commenter_name, text). This method should create a new Comment object and append it to the self.comments list.
    • Finally, update the Article's display() method again. After printing the title, author, and body, it should loop through self.comments and call display_comment() for each comment.

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.