Composite Pattern — Deep Dive
Complete composite implementation
Here’s a production-style file system model using the Composite Pattern:
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Iterator
class FileSystemEntry(ABC):
"""Component — the shared interface."""
@abstractmethod
def size_bytes(self) -> int:
...
@abstractmethod
def display(self, indent: int = 0) -> str:
...
@abstractmethod
def find(self, name: str) -> list["FileSystemEntry"]:
...
@dataclass
class File(FileSystemEntry):
"""Leaf — an individual file."""
name: str
_size: int
def size_bytes(self) -> int:
return self._size
def display(self, indent: int = 0) -> str:
prefix = " " * indent
return f"{prefix}📄 {self.name} ({self._size:,} bytes)"
def find(self, name: str) -> list[FileSystemEntry]:
return [self] if name.lower() in self.name.lower() else []
@dataclass
class Directory(FileSystemEntry):
"""Composite — a directory containing files and subdirectories."""
name: str
children: list[FileSystemEntry] = field(default_factory=list)
def add(self, entry: FileSystemEntry) -> "Directory":
self.children.append(entry)
return self # allow chaining
def remove(self, entry: FileSystemEntry) -> None:
self.children.remove(entry)
def size_bytes(self) -> int:
return sum(child.size_bytes() for child in self.children)
def display(self, indent: int = 0) -> str:
prefix = " " * indent
lines = [f"{prefix}📁 {self.name}/ ({self.size_bytes():,} bytes)"]
for child in self.children:
lines.append(child.display(indent + 1))
return "\n".join(lines)
def find(self, name: str) -> list[FileSystemEntry]:
results: list[FileSystemEntry] = []
if name.lower() in self.name.lower():
results.append(self)
for child in self.children:
results.extend(child.find(name))
return results
def __iter__(self) -> Iterator[FileSystemEntry]:
return iter(self.children)
# Build a tree
root = Directory("project")
src = Directory("src")
src.add(File("main.py", 2048))
src.add(File("utils.py", 1024))
tests = Directory("tests")
tests.add(File("test_main.py", 1536))
root.add(src)
root.add(tests)
root.add(File("README.md", 512))
print(root.display())
# 📁 project/ (5,120 bytes)
# 📁 src/ (3,072 bytes)
# 📄 main.py (2,048 bytes)
# 📄 utils.py (1,024 bytes)
# 📁 tests/ (1,536 bytes)
# 📄 test_main.py (1,536 bytes)
# 📄 README.md (512 bytes)
Notice how size_bytes() works identically whether called on a file or directory. The directory recursively sums its children — which might be files or more directories.
Permission system composite
A practical real-world example: grouping permissions into hierarchical roles.
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
class Permission(ABC):
@abstractmethod
def grants(self, action: str) -> bool:
...
@abstractmethod
def all_actions(self) -> set[str]:
...
@dataclass(frozen=True)
class SinglePermission(Permission):
"""Leaf — grants one specific action."""
action: str
resource: str
def grants(self, action: str) -> bool:
return self.action == action
def all_actions(self) -> set[str]:
return {self.action}
def __str__(self) -> str:
return f"{self.action}:{self.resource}"
@dataclass
class PermissionGroup(Permission):
"""Composite — a named group of permissions."""
name: str
permissions: list[Permission] = field(default_factory=list)
def add(self, perm: Permission) -> None:
self.permissions.append(perm)
def grants(self, action: str) -> bool:
return any(p.grants(action) for p in self.permissions)
def all_actions(self) -> set[str]:
actions: set[str] = set()
for p in self.permissions:
actions |= p.all_actions()
return actions
def __str__(self) -> str:
return f"[{self.name}: {', '.join(str(p) for p in self.permissions)}]"
# Build permission hierarchy
read_posts = SinglePermission("read", "posts")
write_posts = SinglePermission("write", "posts")
delete_posts = SinglePermission("delete", "posts")
manage_users = SinglePermission("manage", "users")
editor = PermissionGroup("editor")
editor.add(read_posts)
editor.add(write_posts)
admin = PermissionGroup("admin")
admin.add(editor) # admin includes all editor permissions
admin.add(delete_posts)
admin.add(manage_users)
# Uniform interface
print(editor.grants("read")) # True
print(editor.grants("delete")) # False
print(admin.grants("delete")) # True
print(admin.all_actions()) # {'read', 'write', 'delete', 'manage'}
The admin group contains the editor group, which contains individual permissions. grants() recurses through the tree transparently.
Traversal strategies
Depth-first iteration
from typing import Iterator
def depth_first(entry: FileSystemEntry) -> Iterator[FileSystemEntry]:
"""Pre-order depth-first traversal."""
yield entry
if isinstance(entry, Directory):
for child in entry:
yield from depth_first(child)
for item in depth_first(root):
if isinstance(item, File):
print(f"Found file: {item.name}")
Breadth-first iteration
from collections import deque
def breadth_first(entry: FileSystemEntry) -> Iterator[FileSystemEntry]:
queue = deque([entry])
while queue:
current = queue.popleft()
yield current
if isinstance(current, Directory):
queue.extend(current.children)
Visitor pattern integration
Add new operations without modifying the composite classes:
from typing import Protocol
class FileSystemVisitor(Protocol):
def visit_file(self, file: File) -> None: ...
def visit_directory(self, directory: Directory) -> None: ...
class SizeReporter:
def __init__(self):
self.total = 0
self.file_count = 0
self.dir_count = 0
def visit_file(self, file: File) -> None:
self.total += file.size_bytes()
self.file_count += 1
def visit_directory(self, directory: Directory) -> None:
self.dir_count += 1
def accept(entry: FileSystemEntry, visitor: FileSystemVisitor) -> None:
if isinstance(entry, File):
visitor.visit_file(entry)
elif isinstance(entry, Directory):
visitor.visit_directory(entry)
for child in entry:
accept(child, visitor)
reporter = SizeReporter()
accept(root, reporter)
print(f"{reporter.file_count} files, {reporter.dir_count} dirs, {reporter.total:,} bytes")
Serialization and deserialization
Composite trees need to survive persistence:
import json
def serialize(entry: FileSystemEntry) -> dict:
if isinstance(entry, File):
return {"type": "file", "name": entry.name, "size": entry._size}
elif isinstance(entry, Directory):
return {
"type": "directory",
"name": entry.name,
"children": [serialize(c) for c in entry.children],
}
raise TypeError(f"Unknown type: {type(entry)}")
def deserialize(data: dict) -> FileSystemEntry:
if data["type"] == "file":
return File(name=data["name"], _size=data["size"])
elif data["type"] == "directory":
d = Directory(name=data["name"])
for child_data in data.get("children", []):
d.add(deserialize(child_data))
return d
raise ValueError(f"Unknown type: {data['type']}")
# Round-trip
exported = json.dumps(serialize(root), indent=2)
imported = deserialize(json.loads(exported))
assert imported.size_bytes() == root.size_bytes()
Safety: preventing cycles
A composite tree must be a DAG (directed acyclic graph). Adding a parent as a child of its descendant creates an infinite loop. Guard against this:
class SafeDirectory(Directory):
def add(self, entry: FileSystemEntry) -> "SafeDirectory":
if entry is self:
raise ValueError("Cannot add a directory to itself")
if isinstance(entry, Directory) and self._is_descendant_of(entry):
raise ValueError("Adding this entry would create a cycle")
self.children.append(entry)
return self
def _is_descendant_of(self, potential_ancestor: "Directory") -> bool:
for child in potential_ancestor.children:
if child is self:
return True
if isinstance(child, Directory) and self._is_descendant_of(child):
return True
return False
Performance considerations
Recursive operations on deep trees can hit Python’s recursion limit (default 1000). For very deep structures:
import sys
# Option 1: Increase limit (careful — can cause stack overflow)
sys.setrecursionlimit(5000)
# Option 2: Convert to iterative with explicit stack
def iterative_size(entry: FileSystemEntry) -> int:
total = 0
stack = [entry]
while stack:
current = stack.pop()
if isinstance(current, File):
total += current._size
elif isinstance(current, Directory):
stack.extend(current.children)
return total
For wide trees (many children per node), the recursive approach is fine. For deep trees (thousands of nesting levels), use the iterative approach.
Tradeoffs
| Advantage | Disadvantage |
|---|---|
| Uniform interface for leaves and composites | Can make leaf-specific operations awkward |
| Natural recursive traversal | Overly general interface — hard to restrict leaf vs composite |
| Easy to add new component types | Type safety is weaker (leaf called with composite methods) |
| Clean tree construction | Cycle prevention requires extra code |
The one thing to remember: The Composite Pattern excels when your domain has natural tree structures — implement a shared interface for leaves and branches, and recursive operations become simple method calls that traverse the entire tree automatically.
See Also
- Python Adapter Pattern How Python's Adapter Pattern works like a travel power plug — making incompatible things work together.
- Python Bridge Pattern Why separating what something does from how it does it keeps your Python code from becoming a tangled mess.
- Python Builder Pattern Why building complex Python objects step by step beats cramming everything into one giant constructor.
- Python Facade Pattern How the Facade Pattern gives you one simple button instead of a confusing control panel in Python.
- Python Flyweight Pattern How the Flyweight Pattern saves memory by sharing common data instead of copying it thousands of times.