What It Is
The Composite pattern treats single things and groups of things through the same interface. A file has a size; a folder full of files (and folders) has a size too — so both implement size(), and code that sums sizes never asks which one it holds.
Composites naturally form trees: groups contain leaves and other groups, to any depth.
Example: Files and Folders
The Item protocol has one method. A file answers with its own size; a folder asks everything inside and adds it up — recursion falls out for free.
One interface for leaf and group
root.size() walks the whole tree without one type check: the folder does not care whether an item is a 4 KB file or another folder hiding ten more.
Output: complete Python program
from dataclasses import dataclass, field
from typing import Protocol
class Item(Protocol):
def size(self) -> int: ...
@dataclass
class File:
name: str
kb: int
def size(self) -> int:
return self.kb
@dataclass
class Folder:
name: str
items: list[Item] = field(default_factory=list)
# A folder's size is the sum of everything inside — files or folders.
def size(self) -> int:
return sum(item.size() for item in self.items)
docs = Folder("docs", [
File("notes.txt", 4),
File("plan.md", 6),
])
root = Folder("root", [
File("readme.md", 2),
docs,
])
print("docs size:", docs.size(), "KB")
print("root size:", root.size(), "KB")
When to Use It
Use it for tree-shaped data where groups and single items answer the same questions — file systems, menus with submenus, UI layouts, organization charts.
Skip it for flat lists. A list with a loop needs no pattern.
Pros and Cons
Pros: client code stays flat and simple; trees of any depth come for free; new leaf or group classes plug into the same protocol.
Cons: the shared protocol can force awkward methods on leaves (what is a file's "add child"?); very general composites become hard to constrain.