We closed module 5 with a confession: hashing is unbeatable when you hand it an exact key, but it falls silent when asked things like "give me the tasks ordered by id" or "all the tasks with priority between 1 and 3". Its keys are scattered on purpose, with no notion of neighborhood or order. To answer those questions we need a structure that keeps the data organized, and that structure is the tree. In this lesson you will learn what a tree is, master its vocabulary (root, leaf, height, depth...), build the TreeNode class for generic trees, and use it to model something TaskFlow has been crying out for: the projects → categories → tasks → subtasks hierarchy. Everything that follows in this module — binary trees, BSTs, AVL trees, B-trees, heaps — rests on the vocabulary and intuition of this lesson.

Contents

  1. From hash to tree: why we need hierarchy and order
  2. What a tree is: definition and properties
  3. Terminology: the vocabulary we'll use all module long
  4. Generic trees in Python: the TreeNode class
  5. Hierarchical TaskFlow: projects, categories, tasks, and subtasks
  6. Traversing a generic tree with recursion
  7. Trees in real life

From hash to tree: why we need hierarchy and order

Let's review what each structure you already have in TaskFlow can do:

Question Structure that answers it Cost
"Give me the task with id T-042" HashTable / dict (module 5) O(1)
"What's the next task to process?" Queue (module 4) O(1)
"Undo the last change" Stack (module 3) O(1)
"Give me the tasks ordered by id" none (hashing scatters)
"Tasks with priority between 1 and 3" none (no neighborhood)
"Which project does subtask X hang from?" none (everything is flat)

The last three rows share something: they ask about relationships between elements (order, range, hierarchical membership), not about an isolated element. Every structure you know so far is linear: each element has, at most, one predecessor and one successor. Trees break that linearity: an element can have several successors. That small difference changes everything.

This module will tackle the three gaps in order: hierarchy today (generic trees), order and ranges in 06-04 (binary search trees), and along the way, in 06-07, we'll settle module 4's promise: opening up heapq from the inside.

What a tree is: definition and properties

A tree is a hierarchical data structure made of nodes connected by edges, satisfying three properties:

  • There is exactly one root node, which has no parent.
  • Every non-root node has exactly one parent.
  • There are no cycles: following edges, you never return to a node you've already visited.

A useful consequence: a tree with n nodes has exactly n - 1 edges (every node except the root contributes the edge that connects it to its parent). And between the root and any node there is a single path — no alternative routes.

A note we'll pick up again in module 7: formally, a tree is a special case of a graph (connected and acyclic). We leave it here as a mention; in general graphs a node may have several "parents" and there will be cycles, and that will demand new techniques.

graph TD
    A["TaskFlow (root)"] --> B["Project: Web"]
    A --> C["Project: Mobile app"]
    B --> D["Backend"]
    B --> E["Frontend"]
    C --> F["iOS"]
    D --> G["T-01: Task API"]
    D --> H["T-02: Database"]
    E --> I["T-03: Login screen"]

Notice that trees in computer science are drawn upside down compared to botanical ones: root at the top, leaves at the bottom. It's pure convention, but it's universal.

Terminology: the vocabulary we'll use all module long

This vocabulary will show up in all seven lessons that follow; it's worth nailing down. Referring to the diagram above:

Term Definition Example in the diagram
Root The only node with no parent TaskFlow
Parent The node another node hangs from directly Backend is the parent of T-01
Child A node that hangs directly from another Web and Mobile app are children of the root
Siblings Nodes that share a parent T-01 and T-02
Leaf A node with no children T-01, T-02, T-03, iOS
Internal node A node with at least one child Web, Backend, Frontend...
Subtree Any node together with all its descendants Backend with T-01 and T-02
Depth of a node Number of edges from the root down to it depth of T-01 = 3
Level The set of nodes at the same depth (the root is at level 0) level 1 = {Web, Mobile app}
Height of the tree Maximum depth of any node 3 (that of T-01)
Degree of a node Its number of children degree of Web = 2

Two subtleties that confuse everyone at first:

  • Depth is measured from the root downward (each node has its own); height is measured from the leaves upward (a node's height is that of the longest path to a leaf below it; the tree's height is the root's height). The root has depth 0 and maximum height; a leaf has height 0 and variable depth.
  • The word subtree is the key to recursion: each child of a node is, in turn, the root of a complete tree. A tree is "a node + a list of smaller trees". That recursive definition is what makes almost every algorithm in this module naturally recursive — the recursion from module 3 is going to work hard here.

Generic trees in Python: the TreeNode class

In a generic (or n-ary) tree, each node can have any number of children. The natural implementation: a value and a list of children. Compare it with the Node from module 2 — there, next was one reference; here, children is a list of references. That's the entire leap from linear to hierarchical.

class TreeNode:
    """A generic tree node: a value and any number of children."""

    def __init__(self, value):
        self.value = value      # here we'll store the task dict, or a string
        self.children = []      # list of TreeNode (empty => it's a leaf)

    def add_child(self, child_node):
        """Hangs another node as a child of this one. Returns the child
        so construction can be chained conveniently."""
        self.children.append(child_node)
        return child_node

    def is_leaf(self):
        return len(self.children) == 0

Point by point:

  • value can be anything: in TaskFlow it will sometimes be a string (a project or category name) and sometimes a task's dict.
  • children starts as an empty list: every node is born a leaf and stops being one when it receives children.
  • add_child returns the added child: that way we can write backend = project.add_child(TreeNode("Backend")) and keep hanging things off backend.
  • There's no wrapping "Tree" class: keeping the root is enough. From it, everything is reachable, just as everything in the LinkedList was reachable from self.head.

Hierarchical TaskFlow: projects, categories, tasks, and subtasks

Until today, TaskFlow stored tasks in flat structures: a linked list, a queue, a hash table. But any real user organizes their work in levels: projects that contain categories, which contain tasks, which in turn have subtasks (remember module 3's nested subtasks, the ones that gave us a RecursionError? They were a tree asking for permission to exist). Let's build it:

def make_task(id, title, priority, status="pending"):
    """Task factory: the same dict as always."""
    return {"id": id, "title": title, "priority": priority, "status": status}

# The root: the whole application
root = TreeNode("TaskFlow")

# Level 1: projects
web = root.add_child(TreeNode("Project: Web"))
mobile = root.add_child(TreeNode("Project: Mobile app"))

# Level 2: categories inside the Web project
backend = web.add_child(TreeNode("Backend"))
frontend = web.add_child(TreeNode("Frontend"))

# Level 3: tasks (the usual dicts, now as a node's value)
t_api = backend.add_child(TreeNode(make_task("T-01", "Task API", 1)))
backend.add_child(TreeNode(make_task("T-02", "Design database", 2)))
frontend.add_child(TreeNode(make_task("T-03", "Login screen", 2)))

# Level 4: subtasks of T-01
t_api.add_child(TreeNode(make_task("T-01a", "GET /tasks endpoint", 1)))
t_api.add_child(TreeNode(make_task("T-01b", "POST /tasks endpoint", 1)))

Notice the elegance: we haven't written a single new line of structure to go from 2 levels to 4. The list of children makes no distinction between "a project containing categories" and "a task containing subtasks"; the hierarchy can grow as deep as the user wants.

Traversing a generic tree with recursion

What good is the tree if we can't query it? Systematic traversals (with names, guaranteed order, and iterative versions) are the subject of 06-03; here we'll see the basic pattern they all derive from: process the node and recurse on each child. First, counting nodes:

def count_nodes(node):
    """How many nodes are in the subtree hanging from `node`."""
    if node is None:
        return 0
    total = 1                          # this node counts
    for child in node.children:
        total += count_nodes(child)    # plus everything hanging from each child
    return total

print(count_nodes(root))   # 10

Read the function in light of the recursive definition of a tree: "the size of a tree is 1 (its root) plus the sum of the sizes of its subtrees". The base case (None → 0) and the calls on strictly smaller structures guarantee termination — exactly the two requirements we studied in module 3. Since there are no cycles and every node has a single parent, each node is visited exactly once: cost O(n).

Now something flashier: printing the tree with indentation, the way tree does in the terminal. Each node's depth becomes indentation:

def show(node, depth=0):
    """Prints the subtree with indentation proportional to depth."""
    if isinstance(node.value, dict):                     # it's a task
        label = f"[{node.value['id']}] {node.value['title']}"
    else:                                                # it's a string (project/category)
        label = node.value
    print("    " * depth + label)
    for child in node.children:
        show(child, depth + 1)                           # children, one level deeper

show(root)

Output:

TaskFlow
    Project: Web
        Backend
            [T-01] Task API
                [T-01a] GET /tasks endpoint
                [T-01b] POST /tasks endpoint
            [T-02] Design database
        Frontend
            [T-03] Login screen
    Project: Mobile app

The depth parameter travels with each recursive call incremented by 1: it's the direct translation of the definition "depth = number of edges from the root". And a note of honesty you already know from module 3: if the tree were absurdly deep (thousands of levels), Python's call stack would protest with a RecursionError; there we learned to convert recursion into iteration with an explicit stack, and in 06-03 we'll apply that technique to traversals.

Trees in real life

Trees are not an academic invention; you probably use several every minute:

  • The file system: /home/joan/projects/taskflow/main.py is a root→leaf path. Folders are internal nodes, files are leaves, and tree is our show function.
  • A web page's DOM: <html> is the root, and every nested tag is a child. When JavaScript reads element.children, it's reading a children list like ours.
  • Org charts: top management as the root, departments as subtrees. "How many people report (directly or indirectly) to X?" is count_nodes(X) - 1.
  • Menus and categories in any application or online store — exactly our hierarchical TaskFlow.
  • Language syntax: the Python interpreter itself turns your code into a tree (the AST) before executing it.

Whenever you see data with a "contains" or "reports to a single superior" relationship at work, think tree.

Common Mistakes and Tips

  • Confusing height and depth. Mnemonic: depth is measured from the root (how many floors below the surface are you?); height from the leaves (how tall are you from the ground?). This gets asked constantly in interviews.
  • Forgetting the base case in recursive functions. Without the if node is None: return 0, calling count_nodes(None) (say, on an empty tree) blows up with AttributeError. Every tree algorithm must decide what to do with the empty tree.
  • Creating cycles by accident. If you add a node as a child of one of its own descendants, you no longer have a tree: recursive traversals will never terminate. Structures with legitimate cycles exist — they're the graphs of module 7 — but they require different techniques.
  • Sharing the children list between nodes. A Python classic: defining def __init__(self, value, children=[]) with a mutable default list makes all nodes share the same list. Always initialize self.children = [] inside __init__.
  • Tip: always keep a reference to the root in a stable variable. Losing a tree's root is losing the whole tree, just like losing head in the linked list.

Exercises

Exercise 1: counting only the tasks

Write count_tasks(node) that returns how many nodes in the subtree contain a task (i.e., whose value is a dict), ignoring projects and categories. On the lesson's tree it must return 5.

Exercise 2: depth of a task

Write depth_of(node, target_id) that returns the depth at which the task with that id sits (the root has depth 0), or None if it doesn't exist. depth_of(root, "T-01a") must return 4.

Exercise 3: leaves of the tree

Write leaves(node) that returns the list of values of all leaves in the subtree. In the lesson's tree, Project: Mobile app is a leaf (it has no content yet), and so are the tasks with no subtasks.

Solutions

Solution 1

def count_tasks(node):
    if node is None:
        return 0
    total = 1 if isinstance(node.value, dict) else 0
    for child in node.children:
        total += count_tasks(child)
    return total

print(count_tasks(root))   # 5  (T-01, T-02, T-03, T-01a, T-01b)

Comment: it's count_nodes with the "1" made conditional. The traversal's structure doesn't change — what changes is what we do at each node. This pattern (fixed traversal, variable action) will repeat throughout the module.

Solution 2

def depth_of(node, target_id, depth=0):
    if node is None:
        return None
    if isinstance(node.value, dict) and node.value["id"] == target_id:
        return depth
    for child in node.children:
        result = depth_of(child, target_id, depth + 1)
        if result is not None:        # already found in this subtree: stop
            return result
    return None

print(depth_of(root, "T-01a"))  # 4
print(depth_of(root, "T-99"))   # None

Comment: the important detail is the if result is not None: return result — as soon as one subtree finds the task, we stop exploring its siblings. Without it, the function would keep searching and return None even after having found the target. Also note that searching a generic tree is O(n): with no rule about where each value lives, you potentially have to look at every node. In 06-04 we'll add that rule and search will drop to O(height).

Solution 3

def leaves(node):
    if node is None:
        return []
    if node.is_leaf():
        return [node.value]
    result = []
    for child in node.children:
        result.extend(leaves(child))
    return result

for v in leaves(root):
    print(v["id"] if isinstance(v, dict) else v)
# T-01a, T-01b, T-02, T-03, Project: Mobile app

Comment: here the useful base case isn't None but is_leaf(): a leaf returns itself; an internal node delegates and concatenates. That Project: Mobile app shows up among the leaves is correct and revealing: "leaf" is a structural property (having no children), not a semantic one (being a task).

Conclusion

You now have the vocabulary (root, leaf, parent, subtree, height, depth, level) and the tool (TreeNode with its list of children) that will support the entire module, and TaskFlow has finally gained its projects → categories → tasks → subtasks hierarchy — with recursive traversals that reuse what you learned in module 3. But look at the solution to exercise 2: searching this tree still costs O(n), and the order and range questions the hash table left unanswered remain unanswered. The road to the answer starts by restricting the tree: if each node has at most two children, with distinguished positions (left and right), tremendously powerful mathematical properties appear — and on top of them we'll build O(log n) search. That restricted tree is the binary tree, the star of the next lesson.

© Copyright 2026. All rights reserved