Concepts / Structural patterns / Composite

Composite

Treating a tree of parts and wholes uniformly.

01The intuition

The problem it solves

A file explorer needs the total size of a folder. Folders contain files and other folders. The first version handles two cases by hand and goes one level deep.

def folder_size(folder):
    total = 0
    for item in folder.items:
        if isinstance(item, File):
            total += item.bytes
        elif isinstance(item, Folder):
            for sub in item.items:
                if isinstance(sub, File):
                    total += sub.bytes
                # and folders inside folders inside folders...
    return total

The code checks types everywhere and still cannot handle a folder three levels deep. Every operation over the tree, size, search, print, permissions, repeats the same type-checking walk. The structure is a tree where a branch and a leaf are different kinds of thing, and the caller is forced to care.

The composite pattern gives leaves and branches the same interface, so a caller can ask a single item, a folder, or the whole tree the same question and get the right answer. The branch answers by asking its children.

Build it one piece at a time

Step 1: name the shared interface. Everything in the tree is a Node that can report its size and its name.

from abc import ABC, abstractmethod

class Node(ABC):
    def __init__(self, name: str): self.name = name
    @abstractmethod
    def size(self) -> int: ...

Step 2: the leaf answers directly.

class File(Node):
    def __init__(self, name: str, bytes: int):
        super().__init__(name); self.bytes = bytes
    def size(self) -> int:
        return self.bytes

Step 3: the branch answers by delegating to its children. It does not know or care whether a child is a file or another folder. Recursion falls out for free.

class Folder(Node):
    def __init__(self, name: str):
        super().__init__(name); self.children: list[Node] = []
    def add(self, node: Node) -> "Folder":
        self.children.append(node); return self
    def size(self) -> int:
        return sum(child.size() for child in self.children)

root = Folder("home").add(File("notes.txt", 300)).add(
    Folder("photos").add(File("a.jpg", 2000)).add(Folder("old").add(File("b.jpg", 500))))
print(root.size())        # 2800, any depth

Step 4: add operations the same way. Printing the tree, counting files or finding a path are each one method on Node, with the leaf doing the simple thing and the folder looping.

class File(Node):
    def tree(self, depth: int = 0) -> str:
        return "  " * depth + self.name + "\n"

class Folder(Node):
    def tree(self, depth: int = 0) -> str:
        return "  " * depth + self.name + "/\n" + "".join(c.tree(depth + 1) for c in self.children)
Stepisinstance checks in callersDepth supportedAdding "count files" costs
Startone per level per operation2another nested walk
1 to 30anyone method on each of 2 classes

Why it works

The composite makes the tree self-similar from the caller's point of view: a folder of files behaves like a file, so the same call works at the root, at a leaf, or anywhere between. The invariant is "every node, leaf or branch, honours the full Node interface". Recursion is not something the caller writes; it is what happens when a branch delegates. This is polymorphism applied to a part-whole structure.

Where it breaks: operations that make sense only for one kind. If Node gains add(child), a File must raise, which violates Liskov substitution; keep child management on Folder only. Cycles are the other trap: a folder that contains an ancestor makes size loop forever, so a tree that can be built from untrusted input needs a visited set or a parent check.

The trade-off you must say out loud

Composite makes the structure uniform at the cost of making leaves and branches look more alike than they are, so a reader cannot tell from a type whether a node has children. It is the right tool for recursive structures with operations that aggregate over the whole; it is the wrong tool for a flat list with two item kinds. Say the recursion depth you expect and whether you need an iterative walk to avoid stack limits. The follow-up is often "how would you add an operation without touching every class", and the answer is the Visitor.

Where it shows up in the 100 Google problems

02Used in these problems