The idea: do the call stack's job yourself

When a function calls itself, the language pushes a stack frame and pops it on return. Nothing about that is magic. You can keep the frames yourself: put them in a list, push one where the code would call, pop one where it would return, and loop until the list is empty. The result is the same, but the frames now live in a list on the heap instead of on the small, fixed-size call stack.

Left: the call stack during total(5) on the balanced tree, with frames total(5) resume at line 3, total(3) resume at line 3 and total(1) on top. Right: the explicit list holds the same three entries as Frame objects, Frame(node=5, at=3), Frame(node=3, at=3) and Frame(node=1, at=2), while the real call stack holds one frame, total(root)
The same three frames: on the call stack, or as objects in a list you own.

What a frame holds

The page sums a binary tree:

def total(node):
    l = total(node.left) if node.left else 0
    r = total(node.right) if node.right else 0
    return node.val + l + r

While total(3) runs, total(5) is paused in the middle of line 2. To finish later it needs three things: its arguments (node = 5), its locals (l and r) and its return address, the place in the code to resume, here "store the value in l and go on at line 3". That is a frame. Real frames also hold saved registers, but these three are what the algorithm needs (Demo: recursion, balanced tree).

The loop: peek, step, pop

Write the frame as an object and the return address as a field at, the next line to run. The loop looks at the top frame and runs one piece of it:

def total(root):
    stack = [Frame(root)]          # node, at=2, l=0, r=0
    while stack:                   # loop until empty
        f = stack[-1]
        if f.at == 2: f.at = 3; push(f.node.left)
        elif f.at == 3: f.at = 4; push(f.node.right)
        else:
            v = f.node.val + f.l + f.r; stack.pop()
            if not stack: return v
            p = stack[-1]          # p.at says where v goes
            if p.at == 3: p.l = v
            else: p.r = v

Two rules make it work. Set at before you push the child, so the frame knows where it is when the child is done. And when you pop, the parent's at tells you whether the value is its l or its r: that is the return address doing its job. Each node takes three loop iterations, so the 7-node tree takes 21, and the list is never deeper than the recursion was (Demo: the same walk with your own stack of frames).

Why the list does not overflow

The call stack has a fixed size: about 8 MB for the main thread on Linux, 512 KB to 1 MB for a Java thread, and Python stops at a depth of 1000 (sys.getrecursionlimit()). The canvas shrinks this to 8 frames. A tree that is a chain 10 nodes deep needs 10, so the 9th call fails with RecursionError (Demo: recursion on a 10-deep chain). The explicit version puts the same 10 frames in a list, which grows until memory runs out, while the real call stack holds just total(root) (Demo: own stack on the chain). Real chains of a million nodes, deep directory trees and long linked lists are where this matters.

The simple case: a stack of nodes

Often you do not need the full frame. If no work waits after a child returns, and the order of the work does not matter, each entry only needs the node:

def total(root):
    stack, s = [root], 0
    while stack:
        node = stack.pop()
        s += node.val
        if node.right: stack.append(node.right)
        if node.left: stack.append(node.left)
    return s

Pushing the right child first means the left one is popped first, so this visits nodes in preorder. On the chain the list never holds more than 1 node (Demo: stack of nodes on the chain). Iterative DFS on a graph is the same pattern.

Three rows comparing what one stack entry stores. Recursion: node, l, r, return address, kept by the language, limit 8 frames on the canvas. Stack of frames: node, at, l, r, kept by you in a list, no fixed limit. Stack of nodes: only the node, works when nothing waits after a child
The less work waits after a child call, the less each entry has to remember.

When to convert

SituationWhat to do
Depth is small and known (balanced tree, divide and conquer on n items)Keep the recursion: log n frames is nothing
Depth can grow with the input (lists, chains, deep graphs, user data)Use an explicit stack
Nothing waits after the child calls, order does not matterStack of nodes (or items)
Work after the call (combine results, postorder)Stack of frames with a state field like at
Only one call, and it is the last thing doneA plain loop: see tail calls

See also Tail Calls and Tail-Call Optimization (the case where no stack is needed at all), Factorial (the call stack of a simple recursion), Depth-First Search and Plain Recursion vs Memoization vs Tabulation (another way to turn recursion into a loop).

Shunting-yard parses expressions with an explicit operator stack instead of recursion: see Infix to Postfix.

What the page leaves out

The canvas uses a call stack of 8 frames and draws one box per frame; real frames differ in size and also hold saved registers and the frame pointer. It leaves out the None calls (the code checks for a child before calling), raising the limit with sys.setrecursionlimit or a bigger thread stack, generators and trampolines as other ways to avoid deep recursion, and how a compiler lays out frames in machine code.