Python Error Guide: 'RecursionError: maximum recursion depth exceeded' — Fix It
Fix Python 'RecursionError: maximum recursion depth exceeded': diagnose runaway or too-deep recursion, spot infinite cycles, and convert to iterative or bounded solutions safely.
- #python
- #automation
- #troubleshooting
- #errors
Stuck on this Bash & Python Automation error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
A script that walks a tree, resolves references, or serializes nested data suddenly aborts with a wall of repeated frames ending in:
RecursionError: maximum recursion depth exceeded
Or a variant that surfaces while Python is comparing or formatting objects:
RecursionError: maximum recursion depth exceeded while calling a Python object
RecursionError: maximum recursion depth exceeded in comparison
Python caps how deep the call stack can go (default limit ~1000 frames) to protect the process from a C-level stack overflow that would crash the interpreter outright. Hitting the cap means a chain of function calls got too deep. Sometimes the recursion is legitimate but just deeper than the default; far more often it is a bug — a missing or wrong base case, or a cycle in the data — and simply raising the limit turns a clean exception into a hard segfault.
Symptoms
- The traceback is enormous, showing the same function name repeated hundreds of times before ending in
RecursionError. - It appears in recursive algorithms: tree/graph traversal, directory walking, JSON/dict serialization, dependency resolution, parsers.
- It can fire inside library internals (
copy.deepcopy,json.dumps,pickle, ORM__repr__) when handed a self-referential object. - The
...while calling a Python object/in comparisonvariants show it triggered during a call or a rich comparison, not at your top-level function. - On some inputs it works and on larger/deeper inputs it fails — a sign the recursion depth scales with input size.
- Raising
sys.setrecursionlimitto a large value replaces the exception with a silent process crash (segfault) — a red flag that depth is the wrong lever.
Common Root Causes
- Missing or unreachable base case — the function never hits its stop condition, so it recurses forever until the cap.
- A cycle in the data — a graph, linked structure, symlink loop, or object that references itself, so traversal never terminates.
- Legitimately deep input — a balanced but genuinely deep tree, or deeply nested JSON, that exceeds ~1000 frames of real work.
- Accidental self-reference in dunder methods —
__repr__,__eq__, or__getattr__that calls itself (e.g.return self.__repr__()orself.__dict__[...]inside__getattr__). - Mutual recursion — two functions calling each other with no terminating branch.
deepcopy/pickle/jsonon a self-referential object — the library recurses into a structure that loops back on itself.- A recursive default/factory — e.g. a
defaultdict(lambda: d)or a config resolver that re-resolves the same key.
Diagnostic Workflow
First, read the bottom and the repeating middle of the traceback — the repeated frame names the recursive function and the exact call that loops:
python3 script.py 2>&1 | tail -25
File "resolve.py", line 18, in expand
return expand(refs[name])
File "resolve.py", line 18, in expand
return expand(refs[name])
[Previous line repeated 994 more times]
RecursionError: maximum recursion depth exceeded
Check the current recursion limit and how deep you actually are when it breaks:
import sys
print(sys.getrecursionlimit()) # default 1000
print(len(sys._current_frames())) # rough sense of live frames
Detect whether the cause is a cycle rather than legitimate depth — instrument the recursive function to log its argument and watch for a repeat:
_seen = set()
def expand(name):
if name in _seen:
raise ValueError(f"cycle detected at {name!r}: {_seen}")
_seen.add(name)
return expand(refs[name])
Post-mortem the failing call interactively to inspect the frame:
python3 -m pdb script.py
# after the crash:
(Pdb) w # 'where' -> the repeating stack
(Pdb) p name # inspect the argument that never changes
If you suspect a self-referential object went into a library, reproduce minimally:
import json
d = {}
d["self"] = d # circular reference
json.dumps(d) # ValueError: Circular reference detected (json guards this)
import copy
copy.deepcopy(d) # deepcopy handles memo; a naive custom __deepcopy__ may not
Example Root Cause Analysis
A config resolver expands ${ref} placeholders and crashes on one environment:
File "config.py", line 22, in resolve
return resolve(values[key])
[Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded
The function:
def resolve(key):
val = values[key]
if val.startswith("${") and val.endswith("}"):
return resolve(val[2:-1]) # follow the reference
return val
Adding the cycle-detection instrumentation showed the argument never changed past a point:
ValueError: cycle detected at 'db_host': {'db_host', 'primary'}
The config had db_host: ${primary} and primary: ${db_host} — a two-key reference cycle introduced by a bad merge. It was not a case for raising the recursion limit; that would only have let it spin longer before crashing the interpreter. The correct fix is to detect the cycle and fail with a clear message, and to bound reference-following:
def resolve(key, _chain=None):
_chain = _chain or []
if key in _chain:
raise ValueError(f"circular reference: {' -> '.join(_chain + [key])}")
val = values[key]
if val.startswith("${") and val.endswith("}"):
return resolve(val[2:-1], _chain + [key])
return val
Now a bad config yields circular reference: db_host -> primary -> db_host instead of a 1000-frame traceback.
Prevention Best Practices
- Guarantee a base case that is provably reached and shrinks the input on every call; test it on the smallest and an empty input.
- Track visited nodes in any traversal over data that could contain cycles (graphs, linked structures, filesystem symlinks) using a
setor a_chainlist. - Prefer iteration for potentially deep structures — an explicit stack/queue (
collections.deque) or a loop avoids the frame limit entirely and is often faster. - Bound the depth explicitly — pass a
depthcounter and raise a domain error at a sane limit (e.g. 50) rather than relying on Python’s global cap. - Only raise
sys.setrecursionlimitdeliberately, for genuinely deep-but-finite input, and pair it with a larger thread stack (threading.stack_size) — never as a fix for a suspected infinite loop, since it trades an exception for a segfault. - Guard dunder methods — make sure
__repr__/__eq__/__getattr__never call themselves; in__getattr__, accessobject.__getattribute__orself.__dict__carefully. - Watch library boundaries — don’t feed self-referential objects to
deepcopy,pickle, or a naive serializer without a memo/seen guard.
Quick Command Reference
import sys
sys.getrecursionlimit() # inspect current cap (default ~1000)
sys.setrecursionlimit(3000) # raise ONLY for finite, genuinely deep input
# Iterative traversal instead of recursion
from collections import deque
def walk(root, children):
stack, seen = deque([root]), set()
while stack:
node = stack.pop()
if node in seen: # cycle guard
continue
seen.add(node)
stack.extend(children(node))
return seen
python3 script.py 2>&1 | tail -25 # read the repeating frame + final error
python3 -m pdb script.py # post-mortem: 'w' for the stack
Conclusion
RecursionError: maximum recursion depth exceeded is Python protecting the process from a stack overflow, and it is usually pointing at a real bug: a base case that never fires or a cycle in your data. Read the repeated frame in the traceback to find the looping call, add cycle detection to confirm, and fix the termination condition. Reach for sys.setrecursionlimit only for input that is genuinely deep but finite — otherwise convert the algorithm to an explicit iterative loop with a visited-set, which sidesteps the frame limit and turns “mysterious crash” into predictable, bounded behavior.
Fixed it? Get 500 Bash & Python Automation & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.