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
- From hash to tree: why we need hierarchy and order
- What a tree is: definition and properties
- Terminology: the vocabulary we'll use all module long
- Generic trees in Python: the
TreeNodeclass - Hierarchical TaskFlow: projects, categories, tasks, and subtasks
- Traversing a generic tree with recursion
- 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) == 0Point by point:
valuecan be anything: in TaskFlow it will sometimes be a string (a project or category name) and sometimes a task'sdict.childrenstarts as an empty list: every node is born a leaf and stops being one when it receives children.add_childreturns the added child: that way we can writebackend = project.add_child(TreeNode("Backend"))and keep hanging things offbackend.- There's no wrapping "Tree" class: keeping the root is enough. From it, everything is reachable, just as everything in the
LinkedListwas reachable fromself.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)) # 10Read 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 appThe 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.pyis a root→leaf path. Folders are internal nodes, files are leaves, andtreeis ourshowfunction. - A web page's DOM:
<html>is the root, and every nested tag is a child. When JavaScript readselement.children, it's reading achildrenlist 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, callingcount_nodes(None)(say, on an empty tree) blows up withAttributeError. 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 initializeself.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
headin 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")) # NoneComment: 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 appComment: 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.
Data Structures Course
Module 1: Introduction to Data Structures
- What Are Data Structures?
- The Importance of Data Structures in Programming
- Types of Data Structures
- Algorithmic Complexity and Big O Notation
- Arrays and Memory: the Foundation of Data Structures
Module 2: Lists
Module 3: Stacks
- Introduction to Stacks
- Basic Stack Operations
- Stack Implementation
- Stack Applications
- Stack Exercises
Module 4: Queues
- Introduction to Queues
- Basic Queue Operations
- Circular Queues
- Priority Queues
- Double-Ended Queues (Deques)
- Queue Exercises
Module 5: Hash Tables and Dictionaries
- Introduction to Hash Tables
- Hash Functions and Collision Resolution
- Dictionaries and Sets in Practice
- Hash Table Exercises
Module 6: Trees
- Introduction to Trees
- Binary Trees
- Tree Traversals
- Binary Search Trees
- AVL Trees
- B-Trees
- Heaps
- Tree Exercises
Module 7: Graphs
- Introduction to Graphs
- Graph Representation
- Graph Search Algorithms
- Shortest Path Algorithms
- Minimum Spanning Trees
- Graph Applications
- Graph Exercises
