Skip to main content

Python Tail Recursion Concept Practice Problems & Exercises

Practice: Tail Recursion Concept

11 problems4 Easy4 Medium3 Hard40-55 min
← Back to lesson

Easy

#1Identify Tail vs Non-Tail RecursionEasy
tail-recursionidentificationconceptual

Classify each function below as tail-recursive or not. Fill in the classify function to return the correct label for each.

Python
# Examine each function and determine: is the recursive call the LAST operation?

def a(n):
    if n == 0: return 0
    return a(n - 1)

def b(n, acc=0):
    if n == 0: return acc
    return b(n - 1, acc + n)

def c(n):
    if n == 0: return 0
    return n + c(n - 1)

def d(lst):
    if not lst: return []
    return [lst[0] * 2] + d(lst[1:])

def e(n):
    if n <= 1: return 1
    return e(n - 1) + e(n - 2)

def classify(name, is_tail):
    return f"{name}: {'tail-recursive' if is_tail else 'NOT tail-recursive'}"

# Fill in True or False for each function
print(classify("a", True))
print(classify("b", True))
print(classify("c", False))
print(classify("d", False))
print(classify("e", False))
Solution
def a(n):
if n == 0: return 0
return a(n - 1)

def b(n, acc=0):
if n == 0: return acc
return b(n - 1, acc + n)

def c(n):
if n == 0: return 0
return n + c(n - 1)

def d(lst):
if not lst: return []
return [lst[0] * 2] + d(lst[1:])

def e(n):
if n <= 1: return 1
return e(n - 1) + e(n - 2)

def classify(name, is_tail):
return f"{name}: {'tail-recursive' if is_tail else 'NOT tail-recursive'}"

print(classify("a", True)) # return a(n-1) — nothing after the call
print(classify("b", True)) # return b(n-1, acc+n) — acc+n computed BEFORE the call
print(classify("c", False)) # n + c(n-1) — addition AFTER the call returns
print(classify("d", False)) # [lst[0]*2] + d(lst[1:]) — concatenation AFTER the call
print(classify("e", False)) # e(n-1) + e(n-2) — addition of TWO recursive calls

Key insight: In b, the expression acc + n is computed before the recursive call is made (it becomes an argument). The recursive call itself is the last operation. In c, the addition n + result happens after c(n-1) returns — making it non-tail.

For e (Fibonacci), there are two recursive calls. Even if one were a tail call, the other cannot be — the results must be added together.

Expected Output
a: tail-recursive\nb: tail-recursive\nc: NOT tail-recursive\nd: NOT tail-recursive\ne: NOT tail-recursive
Hints

Hint 1: A recursive call is a tail call only if it is the LAST operation — nothing happens with its return value.

Hint 2: If there is addition, multiplication, list concatenation, or any other operation AFTER the recursive call returns, it is NOT a tail call.

#2Accumulator Pattern: Sum of ListEasy
accumulatortail-recursionlist-sum

The non-tail recursive sum_list adds the head to the result of recursing on the tail. Convert it to a tail-recursive version sum_list_tail using an accumulator parameter.

Python
# Non-tail recursive: n + sum_list(rest) — addition AFTER recursive call
def sum_list(lst):
    if not lst:
        return 0
    return lst[0] + sum_list(lst[1:])

# TODO: Convert to tail-recursive using an accumulator
def sum_list_tail(lst, acc=0):
    if not lst:
        return acc
    return sum_list_tail(lst[1:], acc + lst[0])

print(f"Non-tail sum: {sum_list([1, 2, 3, 4, 5])}")
print(f"Tail sum: {sum_list_tail([1, 2, 3, 4, 5])}")
print(f"Empty sum: {sum_list_tail([])}")
Solution
def sum_list(lst):
if not lst:
return 0
return lst[0] + sum_list(lst[1:])

def sum_list_tail(lst, acc=0):
if not lst:
return acc
return sum_list_tail(lst[1:], acc + lst[0])

print(f"Non-tail sum: {sum_list([1, 2, 3, 4, 5])}")
print(f"Tail sum: {sum_list_tail([1, 2, 3, 4, 5])}")
print(f"Empty sum: {sum_list_tail([])}")

Trace of sum_list_tail([1, 2, 3, 4, 5]):

sum_list_tail([1,2,3,4,5], 0)
sum_list_tail([2,3,4,5], 1) # acc = 0 + 1
sum_list_tail([3,4,5], 3) # acc = 1 + 2
sum_list_tail([4,5], 6) # acc = 3 + 3
sum_list_tail([5], 10) # acc = 6 + 4
sum_list_tail([], 15) # acc = 10 + 5
-> 15

The key transformation: instead of head + recursive_result, we pass acc + head as the new accumulator. The recursive call is now the last operation — no pending addition after it returns.

Expected Output
Non-tail sum: 15\nTail sum: 15\nEmpty sum: 0
Hints

Hint 1: The accumulator carries the running total. Each recursive call adds the current element to it.

Hint 2: The base case returns the accumulator (not 0), because the accumulator already holds the final answer.

#3Accumulator Pattern: Reverse a ListEasy
accumulatortail-recursionlist-reverse

Write a tail-recursive function reverse_tail that reverses a list using an accumulator. The accumulator builds the reversed list by prepending each element.

Python
def reverse_tail(lst, acc=None):
    if acc is None:
        acc = []
    if not lst:
        return acc
    return reverse_tail(lst[1:], [lst[0]] + acc)

print(f"Reversed: {reverse_tail([1, 2, 3, 4, 5])}")
print(f"Reversed: {reverse_tail(['a', 'b', 'c'])}")
print(f"Reversed: {reverse_tail([])}")
Solution
def reverse_tail(lst, acc=None):
if acc is None:
acc = []
if not lst:
return acc
return reverse_tail(lst[1:], [lst[0]] + acc)

print(f"Reversed: {reverse_tail([1, 2, 3, 4, 5])}")
print(f"Reversed: {reverse_tail(['a', 'b', 'c'])}")
print(f"Reversed: {reverse_tail([])}")

Trace of reverse_tail([1, 2, 3]):

reverse_tail([1, 2, 3], [])
reverse_tail([2, 3], [1]) # acc = [1] + [] = [1]
reverse_tail([3], [2, 1]) # acc = [2] + [1] = [2, 1]
reverse_tail([], [3, 2, 1]) # acc = [3] + [2, 1] = [3, 2, 1]
-> [3, 2, 1]

Why acc=None instead of acc=[]: Default mutable arguments in Python are shared across calls. Using acc=[] would mean the same list object is reused, leading to bugs. The None sentinel pattern avoids this.

Expected Output
Reversed: [5, 4, 3, 2, 1]\nReversed: ['c', 'b', 'a']\nReversed: []
Hints

Hint 1: Prepend each head element to the accumulator list as you recurse through the input.

Hint 2: Start with an empty accumulator. Each step: `acc = [lst[0]] + acc` or `acc = [head] + acc`.

#4Accumulator Pattern: FactorialEasy
accumulatortail-recursionfactorial

Convert the standard recursive factorial to a tail-recursive version using an accumulator.

Python
# Non-tail recursive: n * factorial(n-1)
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

# TODO: Tail-recursive version with accumulator
def factorial_tail(n, acc=1):
    if n <= 1:
        return acc
    return factorial_tail(n - 1, acc * n)

print(f"factorial(5) = {factorial_tail(5)}")
print(f"factorial(10) = {factorial_tail(10)}")
print(f"factorial(0) = {factorial_tail(0)}")
print(f"factorial(1) = {factorial_tail(1)}")
Solution
def factorial_tail(n, acc=1):
if n <= 1:
return acc
return factorial_tail(n - 1, acc * n)

print(f"factorial(5) = {factorial_tail(5)}")
print(f"factorial(10) = {factorial_tail(10)}")
print(f"factorial(0) = {factorial_tail(0)}")
print(f"factorial(1) = {factorial_tail(1)}")

Trace of factorial_tail(5):

factorial_tail(5, 1)
factorial_tail(4, 5) # acc = 1 * 5 = 5
factorial_tail(3, 20) # acc = 5 * 4 = 20
factorial_tail(2, 60) # acc = 20 * 3 = 60
factorial_tail(1, 120) # acc = 60 * 2 = 120
-> 120

The pattern: Move the "work after the call" into an accumulator argument passed into the call. n * factorial(n-1) becomes factorial_tail(n-1, acc * n). The multiplication now happens before the recursive call, not after.

Expected Output
factorial(5) = 120\nfactorial(10) = 3628800\nfactorial(0) = 1\nfactorial(1) = 1
Hints

Hint 1: The accumulator starts at 1 (the multiplicative identity). Each step multiplies acc by n, then decrements n.

Hint 2: Compare: non-tail is `n * factorial(n-1)` — multiplication after the call. Tail is `factorial_tail(n-1, acc * n)` — multiplication before the call (as an argument).


Medium

#5Trampoline: Countdown Without Stack OverflowMedium
trampolinethunkdeep-recursion

Implement a trampoline function and a trampolined count_down that can handle depths far beyond Python's recursion limit. The count_down function should return "done" when it reaches 0.

Python
def trampoline(func, *args):
    result = func(*args)
    while callable(result):
        result = result()
    return result

def count_down(n):
    if n == 0:
        return "done"
    return lambda: count_down(n - 1)

print(f"Countdown result: {trampoline(count_down, 10)}")
print(f"Deep countdown result: {trampoline(count_down, 50000)}")
Solution
def trampoline(func, *args):
result = func(*args)
while callable(result):
result = result()
return result

def count_down(n):
if n == 0:
return "done"
return lambda: count_down(n - 1)

print(f"Countdown result: {trampoline(count_down, 10)}")
print(f"Deep countdown result: {trampoline(count_down, 50000)}")

How it works step by step:

trampoline(count_down, 3):
result = count_down(3) -> returns lambda (not "done")
result = result() -> calls lambda -> count_down(2) -> returns lambda
result = result() -> calls lambda -> count_down(1) -> returns lambda
result = result() -> calls lambda -> count_down(0) -> returns "done"
"done" is not callable -> return "done"

Stack depth at any point: 2 frames — the trampoline loop and the current count_down call. The lambda is created and returned, so count_down(n) exits before count_down(n-1) is called. No accumulation of frames.

Expected Output
Countdown result: done\nDeep countdown result: done
Hints

Hint 1: Instead of calling `count_down(n-1)` directly, return a lambda that calls it: `lambda: count_down(n-1)`. This prevents a new stack frame from being created.

Hint 2: The trampoline function is a while loop that keeps calling the result as long as it is callable. When the result is not callable, it is the final answer.

#6Trampoline Factorial for Large nMedium
trampolineaccumulatorfactorialdeep-recursion

Combine the accumulator pattern with the trampoline to compute factorials for very large n without hitting the recursion limit.

Python
def trampoline(func, *args):
    result = func(*args)
    while callable(result):
        result = result()
    return result

def factorial_t(n, acc=1):
    if n <= 1:
        return acc
    return lambda: factorial_t(n - 1, acc * n)

def factorial(n):
    return trampoline(factorial_t, n)

print(f"factorial(5) = {factorial(5)}")
print(f"factorial(20) = {factorial(20)}")
result = factorial(5000)
print(f"factorial(5000) has {len(str(result))} digits")
Solution
def trampoline(func, *args):
result = func(*args)
while callable(result):
result = result()
return result

def factorial_t(n, acc=1):
if n <= 1:
return acc
return lambda: factorial_t(n - 1, acc * n)

def factorial(n):
return trampoline(factorial_t, n)

print(f"factorial(5) = {factorial(5)}")
print(f"factorial(20) = {factorial(20)}")
result = factorial(5000)
print(f"factorial(5000) has {len(str(result))} digits")

Why this works for n=5000:

Without the trampoline, factorial(5000) would create 5000 stack frames and crash with RecursionError (default limit is 1000). With the trampoline, the stack depth stays at 2 regardless of n.

Performance note: The trampoline adds overhead from creating a lambda object at each step. For production Python code, a simple while loop is faster:

def factorial_iter(n):
result = 1
for i in range(2, n + 1):
result *= i
return result

The trampoline is useful when the algorithm's recursive structure is complex enough that converting to a loop would obscure the logic.

Expected Output
factorial(5) = 120\nfactorial(20) = 2432902008176640000\nfactorial(5000) has 16326 digits
Hints

Hint 1: Combine the accumulator pattern with the trampoline: `factorial_t(n, acc)` returns a lambda when n > 1, and returns `acc` (the final answer) when n <= 1.

Hint 2: The trampoline loop keeps calling thunks until it gets a non-callable result (the integer answer).

#7Convert Recursive Power to IterativeMedium
recursion-to-iterationloop-conversionpower

Convert the recursive power function to an iterative version. The recursive version computes base^exp by multiplying base by the result of power(base, exp - 1). Your iterative version should use a loop.

Python
# Recursive version (crashes for exp > 1000)
def power_recursive(base, exp):
    if exp == 0:
        return 1
    return base * power_recursive(base, exp - 1)

# TODO: Convert to iterative
def power(base, exp):
    result = 1
    for _ in range(exp):
        result *= base
    return result

print(f"power(2, 10) = {power(2, 10)}")
print(f"power(3, 5) = {power(3, 5)}")
print(f"power(5, 0) = {power(5, 0)}")
print(f"power(7, 1) = {power(7, 1)}")
print(f"power(2, 20) = {power(2, 20)}")
Solution
def power(base, exp):
result = 1
for _ in range(exp):
result *= base
return result

print(f"power(2, 10) = {power(2, 10)}")
print(f"power(3, 5) = {power(3, 5)}")
print(f"power(5, 0) = {power(5, 0)}")
print(f"power(7, 1) = {power(7, 1)}")
print(f"power(2, 20) = {power(2, 20)}")

The conversion pattern:

Recursive: Iterative:
base case -> return 1 result = 1 (initial value)
recursive: base * f(exp-1) loop exp times: result *= base
returns after all calls unwind returns result directly

Why iterative is better here:

  • Recursive: O(exp) stack frames, crashes at exp > 1000
  • Iterative: O(1) stack space, handles any exp value
  • For even better performance, use exponentiation by squaring: O(log exp) multiplications
# Bonus: fast power via repeated squaring
def fast_power(base, exp):
result = 1
while exp > 0:
if exp % 2 == 1:
result *= base
base *= base
exp //= 2
return result
Expected Output
power(2, 10) = 1024\npower(3, 5) = 243\npower(5, 0) = 1\npower(7, 1) = 7\npower(2, 20) = 1048576
Hints

Hint 1: The recursive version does `base * power(base, exp - 1)`. The iterative version uses a loop that multiplies `result` by `base` on each iteration.

Hint 2: Initialize `result = 1` and multiply by `base` exactly `exp` times.

#8Iterative Fibonacci with Explicit StateMedium
recursion-to-iterationfibonaccistate-variables

The naive recursive Fibonacci has exponential time complexity and crashes for moderate n. Convert it to an iterative version that uses two state variables (a, b) to track consecutive Fibonacci numbers.

Python
def fib(n):
    if n <= 1:
        return n
    a, b = 0, 1
    for _ in range(n - 1):
        a, b = b, a + b
    return b

print(f"fib(0) = {fib(0)}")
print(f"fib(1) = {fib(1)}")
print(f"fib(10) = {fib(10)}")
print(f"fib(50) = {fib(50)}")
print(f"fib(100) = {fib(100)}")
Solution
def fib(n):
if n <= 1:
return n
a, b = 0, 1
for _ in range(n - 1):
a, b = b, a + b
return b

print(f"fib(0) = {fib(0)}")
print(f"fib(1) = {fib(1)}")
print(f"fib(10) = {fib(10)}")
print(f"fib(50) = {fib(50)}")
print(f"fib(100) = {fib(100)}")

How a, b = b, a + b works:

Step 0: a=0, b=1 (fib(0), fib(1))
Step 1: a=1, b=1 (fib(1), fib(2))
Step 2: a=1, b=2 (fib(2), fib(3))
Step 3: a=2, b=3 (fib(3), fib(4))
Step 4: a=3, b=5 (fib(4), fib(5))
...

Python evaluates the right side (b, a + b) BEFORE assigning to the left side (a, b). This is simultaneous assignment — no temporary variable needed.

Comparison:

Naive recursive: O(2^n) time, O(n) stack — fib(50) takes minutes
Memoized recursive: O(n) time, O(n) stack — fib(1000) hits recursion limit
Iterative: O(n) time, O(1) space — fib(100000) runs in seconds
Expected Output
fib(0) = 0\nfib(1) = 1\nfib(10) = 55\nfib(50) = 12586269025\nfib(100) = 354224848179261915075
Hints

Hint 1: Track two variables: `a` (fib(i-1)) and `b` (fib(i)). Each step: `a, b = b, a + b` advances by one position.

Hint 2: This is equivalent to the accumulator pattern with two accumulators — one for each of the two preceding Fibonacci values.


Hard

#9Trampoline Fibonacci with Two AccumulatorsHard
trampolinefibonacciaccumulatordeep-recursion

Implement Fibonacci using the trampoline pattern with two accumulators. This should handle fib(10000) without stack overflow.

Python
def trampoline(func, *args):
    result = func(*args)
    while callable(result):
        result = result()
    return result

def _fib(n, a=0, b=1):
    if n == 0:
        return a
    if n == 1:
        return b
    return lambda: _fib(n - 1, b, a + b)

def fib(n):
    return trampoline(_fib, n)

print(f"fib(0) = {fib(0)}")
print(f"fib(1) = {fib(1)}")
print(f"fib(10) = {fib(10)}")
print(f"fib(100) = {fib(100)}")
result = fib(10000)
print(f"fib(10000) has {len(str(result))} digits")
Solution
def trampoline(func, *args):
result = func(*args)
while callable(result):
result = result()
return result

def _fib(n, a=0, b=1):
if n == 0:
return a
if n == 1:
return b
return lambda: _fib(n - 1, b, a + b)

def fib(n):
return trampoline(_fib, n)

print(f"fib(0) = {fib(0)}")
print(f"fib(1) = {fib(1)}")
print(f"fib(10) = {fib(10)}")
print(f"fib(100) = {fib(100)}")
result = fib(10000)
print(f"fib(10000) has {len(str(result))} digits")

Trace of fib(5) via trampoline:

trampoline(_fib, 5):
_fib(5, 0, 1) -> lambda # n=5, a=0, b=1
_fib(4, 1, 1) -> lambda # n=4, a=1, b=1
_fib(3, 1, 2) -> lambda # n=3, a=1, b=2
_fib(2, 2, 3) -> lambda # n=2, a=2, b=3
_fib(1, 3, 5) -> 5 # n=1, return b=5

Why two accumulators: Standard Fibonacci needs two preceding values. A single accumulator only carries one value. By threading both a (fib(i)) and b (fib(i+1)), we can compute the next pair in a single tail call: _fib(n-1, b, a+b) advances from (fib(i), fib(i+1)) to (fib(i+1), fib(i+2)).

Expected Output
fib(0) = 0\nfib(1) = 1\nfib(10) = 55\nfib(100) = 354224848179261915075\nfib(10000) has 2090 digits
Hints

Hint 1: Use two accumulators: `a` (current fib value) and `b` (next fib value). Each "step" returns a lambda that calls `_fib(n-1, b, a+b)`.

Hint 2: The base cases: when n == 0 return `a`, when n == 1 return `b`. These are plain values (not lambdas), which stops the trampoline loop.

#10CPS Transformation: Tree SumHard
CPScontinuation-passingtreetrampoline

Implement tree_sum_cps that sums all values in a binary tree using Continuation-Passing Style (CPS) combined with a trampoline. In CPS, instead of returning a result, each function passes it to a continuation (callback). This converts tree recursion (two recursive calls) into a chain of tail calls.

Python
class Node:
    def __init__(self, val, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def trampoline(func, *args):
    result = func(*args)
    while callable(result):
        result = result()
    return result

def _tree_sum(node, cont):
    if node is None:
        return lambda: cont(0)
    # CPS: sum left, then in its continuation sum right, then combine
    def after_left(left_sum):
        def after_right(right_sum):
            return lambda: cont(node.val + left_sum + right_sum)
        return lambda: _tree_sum(node.right, after_right)
    return lambda: _tree_sum(node.left, after_left)

def tree_sum(root):
    return trampoline(_tree_sum, root, lambda x: x)

#       4
#      / \
#     2   6
#    / \ / \
#   1  3 5  7
tree = Node(4, Node(2, Node(1), Node(3)), Node(6, Node(5), Node(7)))
print(f"Tree sum: {tree_sum(tree)}")

# Build a deep left-skewed tree: 1 + 2 + ... + 200 + 100*1990
def build_deep_tree(n):
    if n == 0:
        return None
    node = Node(n)
    current = node
    for i in range(n - 1, 0, -1):
        current.left = Node(i)
        current = current.left
    return node

deep = build_deep_tree(200)
# Add a chain to make it deeper
current = deep
while current.left:
    current = current.left
for i in range(1990):
    current.left = Node(100)
    current = current.left

print(f"Deep tree sum: {tree_sum(deep)}")
Solution
class Node:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right

def trampoline(func, *args):
result = func(*args)
while callable(result):
result = result()
return result

def _tree_sum(node, cont):
if node is None:
return lambda: cont(0)
def after_left(left_sum):
def after_right(right_sum):
return lambda: cont(node.val + left_sum + right_sum)
return lambda: _tree_sum(node.right, after_right)
return lambda: _tree_sum(node.left, after_left)

def tree_sum(root):
return trampoline(_tree_sum, root, lambda x: x)

tree = Node(4, Node(2, Node(1), Node(3)), Node(6, Node(5), Node(7)))
print(f"Tree sum: {tree_sum(tree)}")

deep = build_deep_tree(200)
current = deep
while current.left:
current = current.left
for i in range(1990):
current.left = Node(100)
current = current.left
print(f"Deep tree sum: {tree_sum(deep)}")

How CPS converts tree recursion to a tail-call chain:

Normal recursion (two recursive calls — NOT tail recursive):
def tree_sum(node):
if node is None: return 0
return node.val + tree_sum(node.left) + tree_sum(node.right)
^ ^
Two pending operations after each call!

CPS transformation (single chain of tail calls):
_tree_sum(node, cont):
1. Sum left subtree, passing a continuation that will:
2. Sum right subtree, passing a continuation that will:
3. Combine node.val + left_sum + right_sum and pass to original cont

Each step returns a lambda (thunk), so the trampoline executes them one at a time with constant stack depth. The continuations (closures) are heap-allocated, so they do not consume stack space.

Trade-off: CPS is powerful but harder to read. For production code, an iterative approach with an explicit stack is usually clearer:

def tree_sum_iter(root):
total = 0
stack = [root]
while stack:
node = stack.pop()
if node:
total += node.val
stack.append(node.left)
stack.append(node.right)
return total
Expected Output
Tree sum: 28\nDeep tree sum: 199990
Hints

Hint 1: Continuation-Passing Style (CPS) adds an extra parameter — a function (the "continuation") — that receives the result instead of returning it directly.

Hint 2: For a tree with left and right children: first sum the left subtree, passing a continuation that then sums the right subtree and adds both to the node value.

#11Generic Trampoline DecoratorHard
trampolinedecoratormetaprogrammingsentinel

Build a reusable @trampolined decorator that lets you write tail-recursive functions naturally. The decorator should use a TailCall sentinel class (not callable check) to distinguish "continue recursing" from "return this value." This avoids the bug where returning an actual callable value (like a function) would be mistaken for a thunk.

Python
class TailCall:
    """Sentinel that holds the next function call to make."""
    def __init__(self, func, *args, **kwargs):
        self.func = func
        self.args = args
        self.kwargs = kwargs

def trampolined(func):
    """Decorator: wraps func so recursive self-calls return TailCall objects."""
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        while isinstance(result, TailCall):
            result = result.func(*result.args, **result.kwargs)
        return result
    # Attach TailCall creator so the function can signal "recurse"
    wrapper.call = lambda *args, **kwargs: TailCall(func, *args, **kwargs)
    return wrapper

@trampolined
def count_down(n):
    if n == 0:
        return "done"
    return count_down.call(n - 1)

@trampolined
def factorial(n, acc=1):
    if n <= 1:
        return acc
    return factorial.call(n - 1, acc * n)

@trampolined
def fib(n, a=0, b=1):
    if n == 0:
        return a
    if n == 1:
        return b
    return fib.call(n - 1, b, a + b)

print(f"count_down(100000) = {count_down(100000)}")
result_f = factorial(5000)
print(f"factorial(5000) has {len(str(result_f))} digits")
result_fib = fib(10000)
print(f"fib(10000) has {len(str(result_fib))} digits")
Solution
class TailCall:
"""Sentinel that holds the next function call to make."""
def __init__(self, func, *args, **kwargs):
self.func = func
self.args = args
self.kwargs = kwargs

def trampolined(func):
"""Decorator: wraps func so recursive self-calls return TailCall objects."""
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
while isinstance(result, TailCall):
result = result.func(*result.args, **result.kwargs)
return result
wrapper.call = lambda *args, **kwargs: TailCall(func, *args, **kwargs)
return wrapper

@trampolined
def count_down(n):
if n == 0:
return "done"
return count_down.call(n - 1)

@trampolined
def factorial(n, acc=1):
if n <= 1:
return acc
return factorial.call(n - 1, acc * n)

@trampolined
def fib(n, a=0, b=1):
if n == 0:
return a
if n == 1:
return b
return fib.call(n - 1, b, a + b)

print(f"count_down(100000) = {count_down(100000)}")
result_f = factorial(5000)
print(f"factorial(5000) has {len(str(result_f))} digits")
result_fib = fib(10000)
print(f"fib(10000) has {len(str(result_fib))} digits")

Why TailCall sentinel instead of callable check:

Problem with callable check:
def get_handler(name):
if name == "done":
return lambda x: x # Legitimate return value!
return lambda: get_handler(name[1:]) # Thunk for trampoline

trampoline would call the returned lambda — BUG!

Solution with TailCall sentinel:
@trampolined
def get_handler(name):
if name == "done":
return lambda x: x # Not a TailCall — returned as-is
return get_handler.call(name[1:]) # TailCall object — trampoline continues

Design decisions:

  1. wrapper.call creates a TailCall without executing the function — this is how the decorated function signals "tail-recurse with these args."
  2. isinstance(result, TailCall) is unambiguous — no collision with callable return values.
  3. The trampoline loop is inside wrapper, so the user just calls factorial(5000) naturally.
  4. **kwargs support means keyword arguments work too.

This pattern is production-quality. Libraries like fn.py and toolz use similar approaches.

Expected Output
count_down(100000) = done\nfactorial(5000) has 16326 digits\nfib(10000) has 2090 digits
Hints

Hint 1: Create a `TailCall` wrapper class that holds the function and arguments for the next call. The trampoline loop checks `isinstance(result, TailCall)` instead of `callable(result)` — this avoids the problem of legitimate callable return values.

Hint 2: The decorator wraps the function so that calling it returns a `TailCall` object. A separate `execute` or `run` function drives the trampoline loop.

© 2026 EngineersOfAI. All rights reserved.