Course contents

Composite — trees where leaves and containers share an interface

Sign in to run this lab and save your progress

Learn

Before you start

Composite is one of the most-used patterns in real code — you just haven't been calling it by name.

Every tree-shaped structure where callers say "give me everything under this node" is Composite. File systems. UI trees. Menus. Org charts. JSON. HTML. Every ORM's related-object walks. Every AST evaluator.

The one-line version

Composite lets you treat individual objects and groups of them the same way, through a common interface.

The "no isinstance" signal

The clearest sign your code needs Composite:

if isinstance(child, File):
    result += child.size
elif isinstance(child, Directory):
    for c in child.children:
        # ... recurse manually

That branching is the smell. Composite removes it — the common interface says both types have .total_size(), and Directory's version is just sum(c.total_size() for c in children). No isinstance in sight.

The test in this lesson AST-checks that Directory's methods contain no isinstance or type() calls. That's a strict version of "did you actually apply the pattern?"

Where Composite shows up

Real production examples:

  • File systems. (This lesson.)
  • HTML DOM. Every element has children; text nodes are leaves. .render_to_string() recurses.
  • JSON parsing / serializing. Value (leaf), array (container), object (container). .to_json() walks the tree.
  • UI component trees. React, Vue, Angular — every component tree is Composite.
  • Menu / navigation systems. Items and sub-menus share .render().
  • Query builders. AND(cond1, OR(cond2, cond3)) — a tree of conditions with .evaluate() on each.
  • Test suites. Individual test + test class + test module all support .run().

Any time you write sum(c.total() for c in ...) and the cs might be nested — you're using Composite.

Recursion caveat

Composite implementations usually recurse. In real production:

  • Deep trees (thousands of nesting levels) hit Python's recursion limit. Switch to iterative walks using an explicit stack.
  • Very wide trees are fine — depth is what matters, not total node count.
  • Caching — for trees where computation is expensive and the tree doesn't change often, cache the result on each Directory node. Clear the cache on .add().

Real file systems handle both. Composite is the pattern; the recursion strategy is an implementation detail.

Composite vs Decorator (they can look similar)

Both wrap. Different intent:

  • Composite — wraps a COLLECTION of same-interface objects. Recursive tree shape. Caller sees the whole collection as one thing.
  • Decorator — wraps ONE object with extra behavior. Linear chain shape.

If your wrapper holds a list of children, it's Composite. If it holds ONE inner, it's Decorator.

The interview payoff

"How would you compute the total size of a directory including all subdirectories?" — the naive answer is a recursive function with isinstance checks. The senior answer is: "make File and Directory both implement a Node interface with a total_size method; the recursion falls out of the pattern." That's the Composite insight.

The decision cheat-sheet

Situation Pattern
Tree structure, same operations on leaves and containers Composite
Linear chain of same-interface wrappers Decorator
Family of related objects that must be used together Abstract Factory
Shape mismatch between two objects Adapter
Simple entry point over a complex subsystem Facade

Your task

Composite is for tree-shaped data where the CALLER doesn't need to care whether it's holding a leaf or a container. Both respond to the same method calls; containers just recurse into their children.

The problem

You're modeling a simple file system:

  • A File has a name and a size.
  • A Directory has a name and contains other files AND other directories.

You need to compute:

  • Total size of any node (files: their own size; directories: sum of everything inside)
  • List of files under any node
  • A depth-first walk of the tree

Every operation should work THE SAME WAY on files and directories. A caller writing node.total_size() shouldn't need to know which type it's holding.

Questions to ask about the requirements

  1. Is the structure really tree-shaped? Every node has one parent and 0+ children? Composite fits. Not a tree (arbitrary graph, cycles) → probably not.
  2. Do leaves and containers share the same operations? If yes, Composite unifies them. If they have TOTALLY different APIs, don't force them under one interface.
  3. Do CALLERS want to treat leaves and containers the same way? Or do they always know which one they have? If callers use isinstance checks a lot, the pattern isn't helping.
  4. How deep does the tree get? Composite is recursive; very deep trees may need iterative walks.
  5. Are the leaf and container behaviors really the same shape? Or are you forcing them to look alike?

For our file system: tree-shaped ✓, both support size / walk / listing ✓, callers want uniform treatment ✓. Composite fits.

Different designs, compared

Design A — separate File and Directory classes, no shared interface

class File:
    def size(self): return self._size

class Directory:
    def __init__(self): self._children = []
    def size(self):
        return sum(
            c.size() if isinstance(c, File) else c.size()
            for c in self._children
        )
  • Good: Simple.
  • Bad: isinstance checks in Directory. Every new operation needs a new isinstance chain. Callers who walk the tree also have to isinstance-check. Not extensible — adding a Symlink type would explode the isinstance chains everywhere.
  • When it wins: Never for tree structures. This is the smell Composite fixes.

Design B — plain duck typing (no shared parent)

class Directory:
    def __init__(self): self._children = []
    def size(self):
        return sum(c.size() for c in self._children)  # trust duck typing

Just remove the isinstance check and trust that every child has .size().

  • Good: Works — Directory doesn't care what a child is, as long as it has .size().
  • Bad: No shared interface declared. Type checkers can't help. Every new "node type" has to remember to implement .size() — no formal rule.
  • When it wins: Small internal codebases. Full Composite (with an ABC) is only a small step up.

Design C — full Composite with a Node ABC

class Node(ABC):
    @abstractmethod
    def total_size(self): ...
    @abstractmethod
    def files(self): ...
    @abstractmethod
    def walk(self, prefix=""): ...

class File(Node):
    def __init__(self, name, size):
        self.name = name
        self._size = size
    def total_size(self): return self._size
    def files(self): return [self.name]
    def walk(self, prefix=""):
        yield f"{prefix}{self.name}"

class Directory(Node):
    def __init__(self, name):
        self.name = name
        self._children = []
    def add(self, child): self._children.append(child)
    def total_size(self):
        return sum(c.total_size() for c in self._children)
    def files(self):
        out = []
        for c in self._children:
            out.extend(c.files())
        return out
    def walk(self, prefix=""):
        yield f"{prefix}{self.name}/"
        for c in self._children:
            yield from c.walk(prefix + "  ")
  • Good: Callers write node.total_size() — no branching, no isinstance. Adding a Symlink type is just one new class that implements the Node interface. The required methods are documented in the ABC.
  • Bad: More classes upfront. Deep trees use recursion (fine for realistic sizes).
  • When it wins: All the requirements we gathered.

The locked-in choice: Design C

Design A loses on extensibility. B loses on formal contract. C wins.

Where Composite shows up in real design problems

  • File system (this lesson)
  • UI widget trees — a Panel contains Buttons + more Panels. Panel.render() recursively renders children.
  • Menu structures — a menu has items and sub-menus. Both are MenuNode.
  • Organization charts — an employee has direct reports (who have direct reports).
  • AST / expression trees — every node is either a leaf (literal) or a container (operator with operands). Same interface: .evaluate().
  • JSON documents — value, array, object. serialize() on each.

If you're modeling anything nested, Composite is often the fit.

Mechanics

Copy the Node/File/Directory skeleton from Design C above.

Your task

In main.py build:

  1. Abstract Node(ABC) with abstract methods:
    • total_size(self) → int
    • files(self) → list of file-name strings (leaves-under-here)
    • walk(self, prefix="") → generator yielding strings
  2. File(Node):
    • __init__(self, name, size) stores both
    • total_size returns size
    • files returns [self.name]
    • walk yields f"{prefix}{self.name}" (no trailing slash — files are leaves)
  3. Directory(Node):
    • __init__(self, name) — initializes self.name and self._children = []
    • add(self, child) appends
    • total_size — sum of children's total_size
    • files — flatten children's files lists
    • walk — yield f"{prefix}{self.name}/" first, then recursively walk each child with prefix + " " (two spaces)

Structural tests enforce:

  • Both File and Directory IS-A Node.
  • Deeply nested trees compute totals correctly.
  • files() on a nested structure returns leaves in insertion order.
  • walk() output shows nesting via indent.

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
10-composite-starter