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

AdvantageDisadvantage
Uniform interface for leaves and compositesCan make leaf-specific operations awkward
Natural recursive traversalOverly general interface — hard to restrict leaf vs composite
Easy to add new component typesType safety is weaker (leaf called with composite methods)
Clean tree constructionCycle 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.

pythondesign-patternsoop

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.