Recursion Tracing Practice: Calls, Returns, and Output
A recursive call returns 2. Its caller still has + 2 to do, and the caller above that has + 4 waiting. If you write down only the arguments on the way down, those unfinished operations are easy to lose.
For recursion tracing practice, give each call its own ID and record what it must do after its child returns. Work through the three traces below, then try the six questions on paper before opening an interpreter.

Keep a place for the caller to resume
A frame is one invocation's working space: its arguments, local variables, and current place in the function. Python gives each call its own local namespace, as the Python tutorial on defining functions explains. Two calls with the same argument still have separate frames.
A return exits the current invocation and passes its value to the caller. A bare return, or reaching the end of a function, returns None. See the Python return statement reference. Printing adds text to the output; it doesn't supply that function's return value.
All working snippets here are Python 3 and assume small nonnegative integer inputs. They omit input validation so the trace stays visible. The broken examples are marked separately. Don't run a broken call whose arguments move away from its stopping condition.
We'll count maximum depth as the largest number of this recursive function's frames alive together, including its base-case frame. The module or surrounding test code doesn't count. Total calls counts every invocation, even after it has returned.
Work one call all the way back
def score(n: int) -> int:
if n == 0:
return 2
child: int = score(n - 1)
return child + 2 * n
Trace score(3). Name its invocations A, B, C, and D in the order they start. Each caller pauses at score(n - 1); its assignment to child completes only when that call returns.
| Step | Frame (n) |
Event | Additions still waiting |
|---|---|---|---|
| 1 | A (3) |
Call B: score(2) |
A: + 6 |
| 2 | B (2) |
Call C: score(1) |
A: + 6; B: + 4 |
| 3 | C (1) |
Call D: score(0) |
A: + 6; B: + 4; C: + 2 |
| 4 | D (0) |
Return 2 to C |
A: + 6; B: + 4; C: + 2 |
| 5 | C (1) |
Set child = 2; return 4 to B |
A: + 6; B: + 4 |
| 6 | B (2) |
Set child = 4; return 8 to A |
A: + 6 |
| 7 | A (3) |
Set child = 8; return 14 |
Nothing |
The answer is 14, with four calls and maximum depth four. The function prints nothing. Running print(score(3)) would print 14 because the surrounding print receives the returned value.
The base case finishes D. It doesn't finish A, B, or C. Those callers resume in reverse order, each using its own n: C still has n = 1, even though D had n = 0.
When you get stuck, write the pending expression before following the child. For A, that expression is child + 6. On return, replace child with the received value: 8 + 6.
Printed output has its own timeline
def walk(n: int) -> None:
print("down", n)
if n == 0:
return
walk(n - 1)
print("up", n)
For walk(2), the output is:
down 2
down 1
down 0
up 1
up 2
The first print runs as each frame starts. The second waits for the recursive child to finish. At n = 0, the bare return exits before the second print, so there is no up 0.
Every invocation returns None. That value isn't printed by a plain call to walk(2). If you write print(walk(2)), the same five lines appear, followed by None: the outer print waits for the entire call to finish.
Keep an output column beside the frame ledger. Add a line only when execution reaches a print. Add a return value to the caller's ledger when execution reaches a return or the function ends.
Two children need separate invocation identities
def fork(n: int) -> int:
if n == 0:
return 1
left: int = fork(n - 1)
right: int = fork(n - 1)
return left + right + 1
In fork(2), A completes its entire left call before starting its right call. The assignments make that order explicit.
A: fork(2) -> 7
├── B: fork(1) -> 3
│ ├── C: fork(0) -> 1
│ └── D: fork(0) -> 1
└── E: fork(1) -> 3
├── F: fork(0) -> 1
└── G: fork(0) -> 1
Calls start in order A, B, C, D, E, F, G. They return in order C, D, B, F, G, E, A. B receives 1 from C, then 1 from D, and returns 1 + 1 + 1 = 3. A later receives 3 from each child and returns 7.
There are seven calls, but maximum depth is three. At the deepest point on the first branch, A, B, and C are alive together. C has returned before D starts. B has returned before E starts. Drawing seven nodes doesn't mean seven frames coexist.
Give repeated arguments separate rows: B and E both receive 1, yet both execute their bodies. For more practice counting executions, try the nested-loop counting worksheet.
Six questions to try without running them
Each question stands alone. Copy its function and trace the stated call. Record visible output and the final return separately; for counting questions, include the initial and base calls.
1. Finish the waiting additions
def bump(n: int) -> int:
if n == 0:
return 1
child: int = bump(n - 1)
return child + 4
For bump(2), give the return, total calls, maximum depth, and output. Write the two pending additions before resolving them.
2. Predict both sides of the call
def announce(n: int) -> None:
print(n)
if n == 0:
return
announce(n - 1)
print(n + 10)
Write every output line for announce(2) and give its return value. Would the base invocation print 10?
3. Count calls when a value is used twice
def double_value(n: int) -> int:
if n == 0:
return 1
child: int = double_value(n - 1)
return child + child
For double_value(3), give the return, total calls, and maximum depth. Then replace the last two lines with return double_value(n - 1) + double_value(n - 1). Give the new call count and explain whether the return changes.
4. Find the dropped return
This function is intentionally broken. Its int return annotation states the intended contract: return 5 for every permitted input.
def badge(n: int) -> int:
if n == 0:
return 5
badge(n - 1)
What do badge(0) and badge(2) actually return? Repair one line so the child value reaches the original caller. Does changing the recursive line to print(badge(n - 1)) repair the contract?
5. Check that the base case is reachable
This intentionally broken function should count complete pairs in n items, with n a nonnegative integer:
def pairs(n: int) -> int:
if n == 0:
return 0
return pairs(n - 2) + 1
Trace pairs(4) by hand. For pairs(3), write the first four arguments only; don't execute it. Change the stopping condition so both calls meet the stated contract.
6. Separate inner prints from the outer print
def receipt(n: int) -> int:
if n == 0:
return 2
child: int = receipt(n - 1)
print(child)
return child + 3
Write the output of print(receipt(2)). Which lines come from inside receipt, and which one comes from the outer print?
Check the reason, not just the answer
-
bump(2)returns9; three calls, depth three, no output. The pending work isbump(1) + 4, with another+ 4waiting insidebump(1). The base returns1, so the returning values are1 → 5 → 9. -
announce(2)returnsNoneand prints these five lines:2 1 0 11 12The base prints
0and exits before the final statement. There is no10line. The waiting callers resume with their originalnvalues. -
double_value(3)returns8, with four calls and depth four. The returning values are1 → 2 → 4 → 8. Addingchildtwice reuses one computed value.After the replacement, the return remains
8, but each non-base invocation starts two children. The four levels contain1,2,4, and8calls: 15 calls in total, still with maximum depth four. Both versions print nothing. -
badge(0)returns5;badge(2)returnsNone. The base returns5to then = 1caller, which discards it and reaches the end. Then = 2caller then discards thatNoneand also reaches the end.Replace
badge(n - 1)withreturn badge(n - 1). Changing it toprint(badge(n - 1))would print5, thenNone, forbadge(2), and still returnNone. -
pairs(4)returns2: arguments4 → 2 → 0, values0 → 1 → 2. Starting at3gives3 → 1 → -1 → -3; subtracting two from an odd integer never reaches zero.Change the condition to
if n <= 1:. Zero or one item contains zero complete pairs, so the repairedpairs(3)returns1;pairs(4)still returns2. A decreasing argument must also reach a stopping case. -
print(receipt(2))produces these three lines:2 5 8Inside
receipt, then = 1frame receives2, prints it, and returns5. Then = 2frame receives5, prints it, and returns8. Only then does the outerprintprint8. Callingreceipt(2)on its own prints the first two lines and returns8.
If a result differs from yours, find the first ledger row where the two traces disagree. Repeating only the final number won't reveal whether you lost a return, skipped a pending statement, or counted a reused value as another call. The Boston University recursion tracing lab offers more call-and-return exercises once these examples feel manageable.
Reuse this blank trace sheet
Write the input domain and base condition above the table. Give every new invocation a new ID, even when its argument matches an earlier call. Record each call, print, or return as a separate event; include the printed text or the returned value and its recipient. Add rows as needed.
| Step | Frame (argument) | Event | Work still waiting | Active frame IDs after event |
|---|---|---|---|---|
| 1 | ||||
| 2 | ||||
| 3 | ||||
| 4 | ||||
| 5 | ||||
| 6 |
Start with the initial invocation's entry so it counts toward calls and depth. On a child-call event, add the child to the active list; on a return, remove the returning frame. Below the table, record total calls, maximum depth, final return, and output lines.
For a fresh attempt, use score(2), walk(3), or fork(1) and hide the worked examples. After tracing, check the waiting work on every returning frame before checking your final answer.
Save a repair card after a mistake
Choose a card only if it addresses an error you made. The guide to making better flashcards covers keeping each prompt narrow enough to answer from memory.
| Front | Back |
|---|---|
A caller executes child = f(n - 1), then return child + 4. Its child returns 5. What happens next? |
The caller stores 5, resumes at the next statement, and returns 9. Its own frame still has work to do. |
A recursive function calls f(n - 1) as a bare statement. Does the child's return automatically become the caller's return? |
No. To forward that value, use return f(n - 1). A caller that reaches its end returns None. |
A recursive function subtracts two until n == 0. Why is input 3 a problem? |
Its arguments stay odd: 3, 1, -1, .... Zero is unreachable. The base condition must cover the intended domain; a complete-pairs count can stop at n <= 1. |
After reviewing a card, change a small input and trace again. The next attempt should require you to recover the reasoning, including where each caller resumes.
This worksheet and its original practice examples were developed with substantial AI assistance. An AI coding agent checked the worked examples and safe exercise variants by running isolated snippets with Python 3.12.14 on macOS on October 16, 2026. Those checks covered return values, output, call order, call counts, and depth; they weren't a study of learning outcomes. The broken pairs(3) call was traced without executing it.