RecursionError: maximum recursion depth exceeded means a function called itself about a thousand times without ever stopping. Ninety per cent of the time this is a bug — a missing or unreachable base case — and raising the recursion limit only turns the error into a crash. This guide covers how to tell the two situations apart and what to do in each.
What the error means#
def countdown(n):
print(n)
countdown(n - 1)
countdown(5)
RecursionError: maximum recursion depth exceeded while calling a Python object
Every function call takes a slot on the call stack. Python caps that at around 1000 by default so a runaway function fails with a clear error instead of exhausting memory and taking the process down. The limit is a safety net, not the problem.
Cause 1: no base case#
The example above never stops, because nothing tells it to. Add the condition that ends the recursion:
def countdown(n):
if n <= 0: # base case
return
print(n)
countdown(n - 1) # recursive case, moving towards the base
Every correct recursive function has two parts: a base case that returns without recursing, and a recursive case that moves measurably closer to it. If you cannot point at both in your function, that is the bug.
Cause 2: a base case that is never reached#
This one is harder to spot, because a base case exists:
def countdown(n):
if n == 0:
return
print(n)
countdown(n - 2)
countdown(5) # 5, 3, 1, -1, -3, ... never exactly 0
Starting from an odd number, n skips straight past zero. Use a boundary rather than an equality test:
if n <= 0:
return
The same trap catches float arithmetic, where rounding means a value never lands exactly on the target. Prefer <= and >= over == in any recursive stopping condition.
Cause 3: the value is not actually shrinking#
def flatten(items):
result = []
for item in items:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result
data = [1, [2, 3]]
data.append(data) # the list now contains itself
flatten(data) # RecursionError
Nothing is wrong with the function; the data has a cycle. Track what you have already seen:
def flatten(items, seen=None):
seen = set() if seen is None else seen
if id(items) in seen:
return []
seen.add(id(items))
result = []
for item in items:
if isinstance(item, list):
result.extend(flatten(item, seen))
else:
result.append(item)
return result
The same pattern applies to graphs, linked structures and folder trees containing symbolic links.
How to debug it#
Print the argument on the way in. The pattern tells you which cause you have:
def countdown(n, depth=0):
print(" " * min(depth, 20), "n =", n)
if n <= 0:
return
countdown(n - 1, depth + 1)
If the value never changes, it is cause 3. If it changes but sails past the stopping point, it is cause 2. If there is no stopping point in the code at all, it is cause 1.
You can also check how deep you currently are:
import sys
print(sys.getrecursionlimit()) # 1000 by default
print(len(sys.stack()) if hasattr(sys, "stack") else "n/a")
When raising the limit is the right answer#
Occasionally the recursion is genuinely correct and genuinely deep — parsing a deeply nested document, or walking a long linked list.
import sys
sys.setrecursionlimit(10_000)
Do this only when you have confirmed the recursion terminates. The limit protects a real resource: each Python frame also uses C stack space, and exceeding that gives you a segmentation fault with no traceback at all rather than a clean exception. Raising it to a million is not a fix, it is a delayed crash.
Converting recursion to a loop#
Linear recursion — one call per step — always converts to a simple loop:
# Recursive
def factorial(n):
return 1 if n <= 1 else n * factorial(n - 1)
# Iterative - no stack limit
def factorial(n):
result = 1
for i in range(2, n + 1):
result *= i
return result
Tree recursion converts using an explicit stack, which is the same thing Python was doing for you:
def walk(root):
"""Visit every node without recursion."""
stack = [root]
seen = set()
while stack:
node = stack.pop()
if id(node) in seen:
continue
seen.add(id(node))
yield node
stack.extend(node.get("children", []))
Swap the stack.pop() for a deque.popleft() and you get breadth-first order instead.
When recursion is correct but slow#
Naive Fibonacci recomputes the same values enormously many times:
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(100))
One decorator turns an exponential function into a linear one by remembering results. Note this fixes slowness, not depth — fib(5000) will still hit the recursion limit, because the chain of calls is still 5000 deep.
Questions people ask#
Why is the default limit 1000?
It is a conservative value chosen so that hitting it raises a Python exception before the underlying C stack runs out. The real constraint is memory per frame, which varies by platform, so Python errs on the safe side.
Does Python optimise tail recursion?
No, deliberately. Guido van Rossum has explained that keeping full stack traces for debugging was judged more valuable. That is why deep recursion in Python should generally be rewritten as a loop.
Can I catch RecursionError?
You can, with except RecursionError, but be careful: the stack is nearly full at that moment, so the handler itself has very little room to work in. Fixing the cause is far better than catching it.
Why do I get RecursionError with no recursion in my code?
Usually a property or a __getattr__ that refers to itself — for example a property named value whose body reads self.value. The traceback repeats the same two or three lines, which is the giveaway.
Where to go next#
- Python functions explained — arguments, returns and scope.
- Why is my Python code not working? — reading tracebacks in general.
- Python try/except explained — and why catching this one is a poor idea.