Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 9 min read Last reviewed Jul 2026

Python Error Guide: 'RecursionError: maximum recursion depth exceeded' — Fix It

Quick answer

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
Free toolkit

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 comparison variants 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.setrecursionlimit to 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__() or self.__dict__[...] inside __getattr__).
  • Mutual recursion — two functions calling each other with no terminating branch.
  • deepcopy/pickle/json on 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 set or a _chain list.
  • 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 depth counter and raise a domain error at a sane limit (e.g. 50) rather than relying on Python’s global cap.
  • Only raise sys.setrecursionlimit deliberately, 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__, access object.__getattribute__ or self.__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.

Free download · 368-page PDF

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?

Free download · 368-page PDF

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.