Type-switching every time a structure nests:
def org_cost(node):
if isinstance(node, Employee):
return node.salary
if isinstance(node, Department):
total = 0
for member in node.members:
total += org_cost(member) # and every new node kind edits this
return totalEvery aggregate operation re-implements the traversal, and every new node kind edits every operation. The composite moves the recursion into the container once; operations become one method both node kinds answer.
- Pick the rollup value type. One number is fine; several measures that
should travel together become a small frozen dataclass with
__add__(the org example'sOrgMetricscarries headcount and cost in one pass). - Make leaves plain frozen dataclasses with the operation and nothing else — no child API, ever.
- Use
Compositefor containers (or subclass it to add a name and domain methods). Pass itscombineexplicitly —sumwith astartvalue is usually all you need. - Keep child mutation on the container and let
removeraise on absent children — silent no-ops hide reorg bugs. - Test the rollups through nesting, not just one level: build a small
tree in a fixture, assert totals at every depth, and assert leaves have
no
add(interface honesty is a testable property —not hasattr(leaf, "add")).
from dataclasses import dataclass
from patterns.structural.composite import Composite
@dataclass(frozen=True)
class Task: # a leaf: totals itself, has no child API
hours: int
def total(self) -> int:
return self.hours
team = Composite(sum, [Task(3), Task(5)])
project = Composite(sum, [team, Task(8)])
assert project.total() == 16Protocolinstead of an ABC — the type checker enforces the shared operation; nodes stay free of inheritance.- Frozen dataclass leaves — hashable, comparable, safe to share between branches.
- A metrics dataclass with
__add__rolls several measures up in one traversal instead of one walk per measure. - Generators for traversal:
iter(composite)walks one level; recursive generators (yield from) give yourglob-style deep iteration when you need node access rather than totals.
- Child management on the component interface — the classic form's trap:
leaves inherit an
add()they must refuse at runtime. Keep it on the container only. - Parent pointers by default. They turn a value tree into a mutable graph with invalidation puzzles; add them only when navigation truly needs them.
- Unbounded recursion trust. Deep or user-built trees can hit recursion limits and cycles; if inputs are hostile, traverse iteratively and track visited nodes.
- Mixing structure and presentation (a
render()that formats and recurses and sorts) — keep the tree operation minimal and format outside.
examples/org_chart/ rolls headcount and annual
cost up a nested org chart — run it with:
uv run python -m patterns.structural.composite.examples.org_chart.main