OOP in Python, part 17: Composition

MP 63: An important complement to inheritance.

Note: This post is part of a series about OOP in Python. The previous post looked at the class structure in Matplotlib. The next post shows how to use composition and inheritance together in one project.


Inheritance is a natural concept to introduce when teaching and discussing OOP. As powerful as it is, however, it can lead to problematic models of real-world objects and abstract concepts. Many of these issues can be addressed using a concept called composition.

With inheritance, you model complex systems by writing classes that extend the behavior of other classes. Inheritance is a powerful concept, but it can create problems when the classes become too tightly coupled. For example, making changes to an existing class can affect all subclasses, even the ones you have no control over.

When using composition, you write smaller classes representing individual parts of a system. The resulting connections between classes are much less likely to create issues as each part of the system evolves.

A Library has Books

If you’ve written code that uses OOP there’s a fair chance you’ve used composition before, even if you haven’t heard the term specifically. As an example, consider the context of modeling a library that lends books to its patrons.

The Book class

Here’s the first part of library.py:

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def get_info(self):
        return f"{self.title}, by {self.author}"

In this simplified example, the library only has books. We write a Book class that has two attributes: a title, and an author. The method get_info() returns a string containing both of these attributes.

The Library class

Now we need a class that represents the actual library:

class Library:
    def __init__(self, name, books=None):
        self.name = name

        if books is None:
            self.books = []
        else:
            self.books = books

    def show_books(self):
        print(f"All books in {self.name}:")
        for book in self.books:
            info = book.get_info()
            print(f"- {info}")

When you create an instance of Library you can give it a name, and initialize the library with a list of books. Calling show_books() will display the library’s name, and show information about each book in the library’s catalog.1

Making a library

Let’s create a library and some books, and add them to the library’s catalog:

if __name__ == "__main__":
    library = Library(name="The Climber's Library")

    book = Book(
        title="Freedom of the Hills",
        author="The Mountaineers"
    )
    library.books.append(book)

    book = Book(
        title = "Learning to Fly",
        author = "Steph Davis",
    )
    library.books.append(book)

    library.show_books()

We first create an instance of Library that focuses on books about climbing. We then create two instances of Book. We call library.books.append() to add each book to the library’s catalog. Finally, we show the catalog.

Here’s the output:

$ python library.py 
All books in The Climber's Library:
- Freedom of the Hills, by The Mountaineers
- Learning to Fly, by Steph Davis

There are only two books in the library’s catalog, but this structure would work for thousands or even millions of books.

Book cover image showing a climber standing atop a rocky peak
Mountaineering: The Freedom of the Hills is a classic mountaineering text, that belongs in any climbing-focused library.

A natural use of composition

It’s pretty clear that there’s no use of inheritance in this example. Instead, what we saw here was an example of composition. The Library class uses instances of Books. We started building a model of a library by composing a set of independent classes.

There’s a fair chance that if you asked people with a basic understanding of OOP in Python to write their own implementation for this example, they’d come up with a something similar to this. Composition at its core is not something overly complicated. It’s a way of thinking that comes quite naturally in certain situations.

When discussing OOP, we often talk about is an and has a relationships. We can say that a library has a book, but we’d never say that a library is a book. In situations where you’re modeling has a relationships, composition is a natural fit.

Conclusions

You can start to see some of the strengths of composition even in the short example shown here. For example, there are many ways we might expand the Book class in a real-world implementation. Books can have multiple authors, so the author attribute might become a list called authors. There’s a lot of information we might want to store about an author, so we might write an Author class. If we use instances of Author in the Book class, we’d be using composition again.

When building a system using composition, you can often develop the individual classes as much as you need, while having little impact on the rest of the system. This is one of the main strengths of composition.

In the next post we’ll expand this example using a mix of composition and inheritance.

Resources

You can find the code files from this post in the mostly_python GitHub repository.


  1. You might be wondering why the __init__() method in Library uses an if block when assigning a value to self.books. Briefly, you shouldn’t use an empty list as a default argument. That can cause problems as soon as you have more than one instance of the class.

    For a longer discussion of this issue, see MP #20.