Python

RecursionError: maximum recursion depth exceeded

Written and reviewed by Sahil Srivastav

PythonRecursionRuntime failure
Traceback (most recent call last):
  File "app/tree.py", line 18, in walk
    return walk(node.child)
RecursionError: maximum recursion depth exceeded

What this error actually means

CPython stops recursive calls before the native thread stack is exhausted. Each Python-to-Python call consumes interpreter frames, and the recursion guard raises this exception when the configured limit is reached. The default is deliberately conservative because a deeper call chain can crash the process rather than produce a catchable exception.

The limit is a symptom boundary, not a complexity budget. A missing base case and a valid traversal of a deeply skewed tree both hit it, but raising the limit only helps the second case and can turn the first into a segmentation fault. Recursive code that revisits the same object through a parent pointer is especially deceptive: the input looks finite when printed, but the reachable graph is cyclic.

The traceback’s repeated frames tell you which operation failed, not whether the data or the algorithm is wrong. Count depth, inspect identity edges, and decide whether the operation should be iterative. Most production fixes replace recursion with an explicit stack and retain a visited set for graph-shaped data.

Causes, most common first

  1. 1A missing or unreachable base case. The recursive call does not move toward termination, often because an empty list, null child, or already-processed state is handled after the recursive call.
  2. 2A cycle in data treated as a tree. Parent links, ORM back references, or graph edges lead back to an ancestor. Depth increases forever even though the number of objects is finite.
  3. 3Legitimate depth exceeding the interpreter guard. A generated expression, nested directory, or degenerate binary tree can be thousands of levels deep. The algorithm terminates, but recursion is the wrong storage for that depth.
  4. 4Recursive representation or equality. A custom `__repr__`, `__eq__`, or serializer walks a self-referential object and hides the original application frame behind formatting.

When you see it

  • The traceback repeats the same function dozens of times
  • Only one deeply nested tenant or document triggers the crash
  • JSON or repr output itself raises a recursion error on cyclic objects
  • Increasing the limit makes the process slower or unstable
  • A worker dies while ordinary inputs continue to pass

How to diagnose it

Step 1

Print the limit and count frames

Confirm the environment rather than guessing. The limit applies per interpreter and may have been changed by a test or embedding application.

python - <<'PY'
import sys, traceback
print(sys.getrecursionlimit())
try: walk(root)
except RecursionError:
    traceback.print_exc(limit=12)
PY

Step 2

Detect identity cycles

For graph-like input, track object identity rather than equality. Equality may itself recurse, while `id` is constant for the object’s lifetime.

python - <<'PY'
def visit(node, seen=None):
    seen = set() if seen is None else seen
    if id(node) in seen: raise ValueError('cycle')
    seen.add(id(node))
    for child in node.children: visit(child, seen)
    seen.remove(id(node))
PY

Step 3

Capture the failing input

Log a bounded identifier and depth, not the whole object. A recursive repr can trigger the same exception while you are trying to diagnose it.

The fix

Add a base case before the recursive call and make each call consume input. Test empty, one-element, and cyclic cases explicitly.

For graphs, maintain a visited set keyed by stable node identity or database key. Remove nodes from the active path only when you need to distinguish a cycle from a previously completed node.

Replace deep recursion with an explicit list or deque. This moves depth from the C stack to heap memory, where it can be bounded and monitored.

Raise `sys.setrecursionlimit` only for a measured, finite depth that you own. Test the maximum input in the same process model; the setting is not a substitute for cycle detection.

If serialization is the failing operation, use a serializer with reference handling or project objects to IDs and fields rather than traversing ORM back references.

def walk(root):
    stack = [root]
    seen = set()
    while stack:
        node = stack.pop()
        key = id(node)
        if key in seen:
            continue
        seen.add(key)
        yield node
        stack.extend(reversed(node.children))

How to stop it coming back

  • Fuzz nested and cyclic structures, not only balanced trees
  • Set an explicit maximum document depth at input validation
  • Keep recursion limits unchanged in production unless a design review records why
  • Emit depth histograms for user-controlled nested data
  • Treat recursive repr and serializers as production code with cycle tests

Practise production debugging in a real repository

Reading about a failure and reproducing one are different skills. Gronex ships broken backend repositories with failing test suites that encode the real invariant, so you debug from evidence instead of memorising symptoms.

FAQ

Should I call sys.setrecursionlimit(100000)?

No. It can postpone a valid-depth failure, but for an accidental cycle it permits more frames before a native stack crash. Prove the maximum depth and prefer iteration.

Is this always caused by infinite recursion?

No. A terminating traversal of a very deep structure can exceed the guard. The repeated frames and input depth distinguish it from a call that never makes progress.

Why does printing the object fail too?

The printer may follow the same cycle through repr or a custom serializer. Print IDs, types, and bounded fields instead of the complete object.

Related

Other errors engineers hit next to this one

Full error and symptom index →