skip to content

Does CPython perform tail-call elimination on a tail-recursive Python function?

level: middleimportance: should knowfreq 48%

answer

  1. Position of the call changes nothing here
  2. Every call still builds a frame
  3. The counter does not care about tail position
  4. 3.11 made calls cheap, not free of frames
  5. A while loop is the real answer

basics

~10 s

No. CPython pushes a fresh frame for every call, tail position or not, so a tail-recursive function still counts against the recursion limit and still raises RecursionError. Rewrite that shape as a loop.

solid answer

~50 s

CPython has never eliminated tail calls and there is no plan to. A `return f(...)` in tail position compiles to an ordinary call: a new Python frame is pushed, the depth counter increments, and the frame is only popped when the callee returns. So writing a function in accumulator-passing style buys nothing in Python — it fails at the same depth as any other recursion. The fix for a tail-recursive shape is a `while` loop that rebinds the parameters each pass. Two changes are commonly mistaken for tail-call elimination: 3.11 inlined Python-to-Python calls so a Python call no longer nests a C frame, which made calls cheaper and a raised limit far safer but left the frame count untouched; and 3.14 ships an optional tail-call *interpreter* build, a C dispatch technique between bytecode handlers, with no effect on Python-level recursion.

code

python · 18 lines
python
import sys

def countdown(n):
    if n == 0:
        return "done"
    return countdown(n - 1)      # tail position, and CPython still pushes a frame

try:
    countdown(sys.getrecursionlimit() + 100)
except RecursionError:
    print("tail position did not save us")

def countdown_loop(n):
    while n:
        n -= 1
    return "done"

print(countdown_loop(1_000_000))

go deeper

for a junior

Remember the one-line fact: Python does not optimise tail recursion, so putting the recursive call last saves nothing. If depth grows with the input, write a loop.

for a middle

Explain the mechanics — a tail call compiles to an ordinary call, a frame is pushed, the depth counter increments — and show the mechanical rewrite of a tail-recursive function into a while loop that rebinds its parameters.

for a senior

An interviewer expects you to separate the lookalikes: 3.11's call inlining made calls cheap and a raised limit safer without changing frame counting, and 3.14's tail-call interpreter is an eval-loop dispatch technique in C. Name the risk pattern too: depth proportional to input size.

for a principal

Own the guidance you set for the codebase: where recursion is acceptable (bounded, logarithmic depth), where it must be iterative, and why you would not adopt trampolines as a house style given what they cost in tracebacks, profiling and readability.

## The claim, precisely A *tail call* is a call whose result is returned immediately, with nothing left for the caller to do afterwards — `return helper(n - 1, acc + n)` is one; `return 1 + helper(n - 1)` is not, because the addition still has to happen after the callee returns. *Tail-call elimination* is the implementation technique of reusing the caller's frame for such a call instead of pushing a new one, which makes tail recursion run in constant stack space. **CPython does not do this.** Not for direct recursion, not for mutual recursion, not under any flag or optimisation level. The compiler emits an ordinary call instruction, the interpreter pushes a new Python frame, and the thread's recursion counter goes up by one exactly as it would for any other call. Consequently a tail-recursive function raises `RecursionError` at the same depth as a non-tail-recursive one, and rewriting a function into accumulator-passing style — the transformation that makes a function stack-safe in a language that does eliminate tail calls — changes nothing about its depth behaviour in Python. CPython's maintainers have declined the optimisation deliberately rather than by oversight; the usual reason given is that it would discard frames that tracebacks, debuggers and profilers depend on. Whatever you think of the tradeoff, the practical takeaway for an interview is unambiguous: in Python, depth-safety comes from not recursing, not from where you put the call. ## What to do instead For a genuinely tail-recursive shape the mechanical rewrite is a loop that rebinds the parameters: ```python def countdown(n): if n == 0: return "done" return countdown(n - 1) def countdown_loop(n): while n: n -= 1 return "done" ``` The loop runs in one frame at any depth, and it is also faster: even after 3.11 made calls substantially cheaper, a Python call still costs far more than a loop iteration, because it builds and tears down a frame, binds arguments and re-enters the evaluation machinery. A *trampoline* is the other classic workaround: the recursive function returns a small callable or a data record describing the next call instead of making it, and a driver loop repeatedly invokes what it gets back until a real value appears. It genuinely bounds the stack, but it changes the function's contract, flattens tracebacks into one uninformative frame, confuses profilers, and is slower than the loop it is emulating. It is a curiosity worth knowing about, not a default. ## Two changes that are not tail-call elimination **The 3.11 call inlining.** Before 3.11, a Python function calling another Python function re-entered the interpreter's evaluation function recursively, so every Python-level call also consumed a C stack frame. 3.11 changed that: a Python-to-Python call now continues in the same interpreter loop. Calls got cheaper, tracebacks got better, and — importantly — the C stack stopped growing in lockstep with Python recursion, which is why raising the recursion limit is far safer on modern versions than it was on 3.10. What did **not** change is the frame count: each call still creates a Python frame and still increments the depth counter, so the default limit still stops you at the same place. **The 3.14 tail-call interpreter.** CPython 3.14 added an optional build configuration in which the evaluation loop's per-opcode handlers hand control to one another using C tail calls rather than looping through a central dispatch. On a compiler that supports it, this yields a modest interpreter speedup. The name causes real confusion in interviews: it describes how *bytecode handlers* dispatch inside the interpreter's C code. It has nothing to do with Python-level tail calls, does not reuse Python frames, and does not change how deep a recursive Python function can go. Neither does the experimental JIT, which is off by default and is a code-generation change, not a calling-convention change. ## How this surfaces in real code The failure mode is a recursion that is correct on every input you have seen and fatal on the input you have not. A function that walks a chain one element per call is fine on test data a few dozen elements long and dies on a production chain of tens of thousands, and no amount of tail-position tidiness averts it. When a recursion's depth is proportional to input size rather than to something logarithmic like a balanced tree's height, treat the recursion itself as the risk and convert the linear part to iteration.

  • CPython 3.11 made Python-to-Python calls cheaper. Did that let recursion go deeper?
    Not by itself. The default limit is still 1000 and every call still creates a Python frame that counts against it. What changed is the cost of exceeding it deliberately: before 3.11 each Python call also nested a C frame, so raising the limit far above the default risked a hard crash, while on modern versions pure-Python recursion no longer grows the C stack in lockstep, which makes a raised ceiling much safer.
  • Does the tail-call interpreter added in CPython 3.14 change any of this?
    No. It is a build-time choice about how the evaluation loop dispatches between bytecode handlers, which pass control to one another with C tail calls instead of returning to a central switch. It can make the interpreter modestly faster on a supporting compiler. It does not reuse Python frames, does not touch the recursion counter, and does not make a recursive Python function stack-safe.
  • Would a trampoline decorator make deep recursion safe in Python?
    It can, by having the function return a description of the next call rather than making it, with a driver loop applying those until a real value comes back. The cost is real: the function's contract changes, tracebacks collapse to the driver frame, profilers and debuggers lose the call structure, and it runs slower than the plain loop it emulates. Prefer the loop unless the recursion is genuinely mutual and awkward to flatten.

saying these in an interview costs you the question

  • Says Python optimises tail recursion like a functional language
  • Believes a call in tail position avoids creating a frame
  • Thinks 3.11's call inlining removed the recursion limit
  • Confuses 3.14's tail-call interpreter build with Python-level tail calls
  • Claims sys.setrecursionlimit turns tail-call optimisation on
  • Assumes the experimental JIT changes call or frame semantics

context