Skip to main content

Traversing Trees: Eager vs Lazy Approaches

Tree structures are everywhere โ€” from file systems and HTML DOMs to nested comment threads and syntax trees. Traversing them efficiently and clearly is a common but often overlooked challenge.

In this article, we'll compare eager and lazy approaches to tree traversal in Python. Specifically, we'll explore:

  • What generator-based traversal looks like
  • How it compares to traditional list-returning recursion
  • Why the lazy approach might be more powerful than it first appears

๐Ÿงฐ Tree Structure Setupโ€‹

Let's use a basic tree class with value and children:

class Node:
def __init__(self, value, children=None):
self.value = value
self.children = children or []

Now let's create a simple tree:

root = Node("A", [
Node("B", [Node("D"), Node("E")]),
Node("C")
])

Visualized:

    A
/ \
B C
/ \
D E

โœ… Version 1: Eager Traversal (Return a List)โ€‹

def traverse_to_list(node):
result = [node.value]
for child in node.children:
result.extend(traverse_to_list(child))
return result

Usage:

for val in traverse_to_list(root):
print(val)

Output:

A
B
D
E
C

This version accumulates the full result list up front before returning anything.


โœจ Version 2: Lazy Traversal (Using a Generator)โ€‹

def traverse(node):
yield node.value
for child in node.children:
yield from traverse(child)

Usage:

for val in traverse(root):
print(val)

Output is identical:

A
B
D
E
C

But behavior is different:

  • Values are yielded one-by-one
  • No list is created
  • Memory usage is minimal

๐Ÿ“Š When to Use Eachโ€‹

SituationEager ListLazy Generator
Small treesโœ… Simple and fastโœ… Also fine
Large treesโŒ Memory-heavyโœ… Better performance
Want full resultโœ… Easy to getโŒ Must convert to list
Filter/map mid-traversalโŒ Requires extra stepsโœ… Natural with generator expressions
Streaming use caseโŒ Not viableโœ… Ideal

๐Ÿงช Bonus: Filtering While Traversingโ€‹

Because generators yield values lazily, you can filter on the fly:

for val in traverse(root):
if val.startswith("D"):
print("Found:", val)

Or use a generator expression:

found = (v for v in traverse(root) if v.startswith("D"))
print(next(found)) # Found: D

๐Ÿ” What's Next?โ€‹

In real-world scenarios, you usually start with nested dictionaries, flat lists, or JSON โ€” not pre-built Node trees. Check out this article for more on building trees from raw data


๐Ÿ”„ TL;DRโ€‹

  • Generator-based tree traversal is clean, elegant, and memory-efficient.
  • Recursive list-returning traversal is simple but can become inefficient.
  • Use generators when you want laziness, streaming, or in-place filtering.

Both techniques have their place โ€” the key is knowing which one matches your use case.