RecursionError: maximum recursion depth exceeded means your code either has no base case, has an unreachable base case, or legitimately needs deeper recursion. Fix the base case first; convert to iterative with an explicit stack for deep recursion; use sys.setrecursionlimit() only as a temporary measure.sys.setrecursionlimit(N) to raise the limit temporarily; convert to iterative with stack = [initial]; while stack: item = stack.pop() for a permanent fix.sys.setrecursionlimit(10**6) without increasing the OS thread stack size -- this causes a segfault instead of a clean Python exception.sys.getrecursionlimit() to 1000. This includes all nested Python function calls, not just the recursive function itself. Framework overhead (Django, Flask) may consume 50-100 frames before your code runs. [src1]sys.setrecursionlimit() only controls Python-level recursion. C-level recursion is governed by Py_C_RECURSION_LIMIT, which is hardcoded. [src7]sys.setrecursionlimit(100000) without increasing OS stack size causes segfault. [src1, src3]resource.setrlimit(resource.RLIMIT_STACK, ...) is not available on Windows. Use threading.stack_size() for new threads instead. [src1]| # | Cause | Likelihood | Signature | Fix |
|---|---|---|---|---|
| 1 | Missing base case | ~30% | RecursionError immediately or after small input | Add if check that returns without recursing |
| 2 | Unreachable base case (off-by-one, wrong condition) | ~20% | Works for small inputs, fails at larger ones | Debug base case condition; print args at each call |
| 3 | Accidental recursion (__repr__, __str__, __eq__) | ~15% | RecursionError in repr() or comparison | Avoid self-referencing in dunder methods |
| 4 | Legitimate deep recursion (tree/graph DFS) | ~12% | Fails on deep/large inputs only | Convert to iterative with explicit stack |
| 5 | Infinite mutual recursion (A calls B calls A) | ~8% | Traceback alternates between two functions | Break cycle; add base case in one function |
| 6 | Property getter calls itself | ~5% | RecursionError on attribute access | Use self._attr backing attribute |
| 7 | @lru_cache with deep first call | ~3% | First call fails, subsequent work | Warm cache incrementally: for i in range(n): fib(i) |
| 8 | JSON/XML parsing of deeply nested data | ~3% | Fails on specific input files | Use iterative parser or set limit for known depth |
| 9 | Metaclass or decorator recursion | ~2% | RecursionError during class definition | Check decorator/metaclass for circular calls |
| 10 | pickle/copy.deepcopy on recursive structures | ~2% | RecursionError during serialization | Implement __reduce__ or flatten structure |
START
├── Does the traceback show the SAME function repeating?
│ ├── YES -- Is there a base case (if/return without recursing)?
│ │ ├── NO -> Add a base case. See Step 1.
│ │ └── YES -> Is the base case reachable? Add print(args) to check.
│ │ ├── NO -> Fix the condition. See Step 2.
│ │ └── YES -> Recursion is legitimately deep (continue below)
│ └── NO -- Do TWO functions alternate in the traceback?
│ ├── YES -> Mutual recursion. Break the cycle. See Step 3.
│ └── NO -> Check for __repr__/__str__/__eq__ in traceback
├── Does traceback mention __repr__, __str__, __eq__, or property?
│ ├── YES -> Accidental self-reference in dunder method. See Anti-Pattern #4.
│ └── NO (continue below)
├── Is recursion depth < 1000 but still failing?
│ ├── YES -> Framework overhead consuming frames. Increase limit by 500-1000.
│ └── NO (continue below)
├── Do you need depth > 10000?
│ ├── YES -> MUST convert to iterative with explicit stack. See Step 5.
│ └── NO (continue below)
├── Do you need depth 1000-10000?
│ ├── YES -> Option A: sys.setrecursionlimit(N+100). Option B: Convert to iterative.
│ │ └── If setting limit: also increase OS stack size or run in a new thread.
│ └── NO (continue below)
└── DEFAULT -> Check for accidental recursion (property calling itself, __init__ calling __init__).
Most RecursionErrors are caused by a missing or unreachable base case. Read the traceback to find the repeating function call and verify its termination condition. [src4]
# Example: broken base case -- never reaches 0 for negative input
def factorial(n):
if n == 0: # Bug: what if n < 0?
return 1
return n * factorial(n - 1)
# factorial(-1) -> RecursionError
# FIXED: handle edge cases
def factorial(n):
if n < 0:
raise ValueError(f"factorial not defined for negative: {n}")
if n <= 1: # Covers 0 and 1
return 1
return n * factorial(n - 1)
Verify: factorial(0) returns 1, factorial(-1) raises ValueError, factorial(10) returns 3628800.
When the base case exists but is never reached, log the arguments at each recursive call. [src4]
import sys
def find_path(graph, start, end, path=None):
if path is None:
path = []
path = path + [start]
if start == end:
return path
for node in graph.get(start, []):
if node not in path: # Prevents infinite loops in cyclic graphs
result = find_path(graph, node, end, path)
if result:
return result
return None
Verify: Tracing output shows why the base case is not reached.
When function A calls B and B calls A without termination. [src4]
# FIXED: guard against negative input
def is_even(n):
n = abs(n)
if n == 0:
return True
return is_odd(n - 1)
def is_odd(n):
n = abs(n)
if n == 0:
return False
return is_even(n - 1)
Verify: is_even(-4) returns True, is_odd(3) returns True.
For problems like Fibonacci where the same subproblems recur, @lru_cache reduces actual recursion depth dramatically. [src5]
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
# Warm up cache incrementally to avoid deep first call
def fib_safe(n):
chunk = 500
for target in range(chunk, n + 1, chunk):
fib(target)
return fib(n)
print(fib_safe(10000)) # Works
Verify: fib_safe(10000) completes without RecursionError.
The universal fix for deep recursion. Replace the call stack with a list. [src6]
# ITERATIVE -- handles any depth, uses O(n) heap memory
def dfs_iterative(graph, start):
visited = set()
stack = [start]
while stack:
node = stack.pop()
if node in visited:
continue
visited.add(node)
for neighbor in reversed(graph.get(node, [])):
if neighbor not in visited:
stack.append(neighbor)
return visited
Verify: Iterative version handles graphs with 100K+ nodes.
When you cannot refactor immediately and know the required depth. [src1]
import sys
import threading
# Option A: Raise limit (risk of segfault if too high)
sys.setrecursionlimit(5000)
# Option B: Run deep recursion in a thread with larger stack
def run_deep_recursion():
sys.setrecursionlimit(50000)
result = deep_recursive_function(30000)
return result
# Set thread stack size to 64 MB
threading.stack_size(67108864) # 64 * 1024 * 1024
thread = threading.Thread(target=run_deep_recursion)
thread.start()
thread.join()
Verify: sys.getrecursionlimit() returns the new value.
When you have a naturally tail-recursive function and want to avoid stack growth. [src4, src6]
def trampoline(fn):
def wrapper(*args, **kwargs):
result = fn(*args, **kwargs)
while callable(result):
result = result()
return result
return wrapper
@trampoline
def factorial_tr(n, acc=1):
if n <= 1:
return acc
return lambda: factorial_tr(n - 1, n * acc)
print(factorial_tr(50000)) # Works -- constant stack depth
Verify: factorial_tr(50000) completes without RecursionError.
# Input: Binary tree root node
# Output: List of values in in-order traversal
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def inorder_iterative(root):
result, stack = [], []
current = root
while current or stack:
while current:
stack.append(current)
current = current.left
current = stack.pop()
result.append(current.val)
current = current.right
return result
# Input: 2D grid, start position, new color
# Output: Grid with connected region recolored
def flood_fill(image, sr, sc, new_color):
rows, cols = len(image), len(image[0])
old_color = image[sr][sc]
if old_color == new_color:
return image
stack = [(sr, sc)]
while stack:
r, c = stack.pop()
if (0 <= r < rows and 0 <= c < cols
and image[r][c] == old_color):
image[r][c] = new_color
stack.extend([(r+1,c),(r-1,c),(r,c+1),(r,c-1)])
return image
# Input: Any recursive function
# Output: Equivalent iterative with explicit stack
def flatten_nested(data):
"""Flatten arbitrarily nested lists -- iterative."""
result = []
stack = [iter(data)]
while stack:
try:
item = next(stack[-1])
if isinstance(item, list):
stack.append(iter(item))
else:
result.append(item)
except StopIteration:
stack.pop()
return result
# flatten_nested([1, [2, [3, 4], 5], 6]) -> [1, 2, 3, 4, 5, 6]
# BAD -- causes segfault instead of clean RecursionError [src1, src3]
import sys
sys.setrecursionlimit(10**6)
def deep(n):
if n == 0: return 0
return deep(n - 1) + 1
deep(500000) # Segmentation fault (core dumped)
# GOOD -- iterative version with no stack limit [src6]
def deep(n):
result = 0
for _ in range(n):
result += 1
return result
deep(500000) # Works instantly
# BAD -- recursion where a simple loop suffices [src4]
def sum_list(lst, index=0):
if index >= len(lst):
return 0
return lst[index] + sum_list(lst, index + 1)
sum_list(range(2000)) # RecursionError
# GOOD -- natural loop, no recursion overhead [src4]
def sum_list(lst):
return sum(lst) # Built-in, C-optimized
# BAD -- self.name triggers the getter recursively [src2]
class User:
def __init__(self, name):
self.name = name
@property
def name(self):
return self.name # Infinite recursion!
@name.setter
def name(self, value):
self.name = value # Infinite recursion!
# GOOD -- separate backing attribute [src2]
class User:
def __init__(self, name):
self.name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
# BAD -- circular reference causes infinite repr [src2]
class Node:
def __init__(self, val, neighbors=None):
self.val = val
self.neighbors = neighbors or []
def __repr__(self):
return f"Node({self.val}, neighbors={self.neighbors})"
a, b = Node(1), Node(2)
a.neighbors.append(b)
b.neighbors.append(a)
print(a) # RecursionError
# GOOD -- repr does not recurse into neighbors [src2]
class Node:
def __init__(self, val, neighbors=None):
self.val = val
self.neighbors = neighbors or []
def __repr__(self):
neighbor_ids = [n.val for n in self.neighbors]
return f"Node({self.val}, neighbors={neighbor_ids})"
@lru_cache does not prevent deep recursion: Memoization reduces redundant calls but the first cold call to fib(2000) still recurses 2000 deep. Warm up incrementally. [src5]sys.setrecursionlimit affects all threads: The limit is process-wide. Prefer running deep recursion in a dedicated thread with threading.stack_size() set appropriately. [src1]sys.setrecursionlimit changed in Python 3.12: Since 3.12, only controls Python frames. C-level recursion has a separate hardcoded limit. [src7]yield from create frame chains: yield from recursive_generator() creates generator frames that count toward the limit. Use iterative generators for deep data. [src4]# Check current recursion limit
python -c "import sys; print(sys.getrecursionlimit())"
# Check OS thread stack size (Linux/macOS)
ulimit -s
# Find safe maximum recursion depth for your system
python -c "
import sys
sys.setrecursionlimit(100000)
def probe(n):
try:
if n == 0: return 0
return probe(n - 1)
except RecursionError:
return n
print(f'Max safe depth: ~{probe(99999)}')
"
# Check if resource module is available (Unix only)
python -c "
try:
import resource
soft, hard = resource.getrlimit(resource.RLIMIT_STACK)
print(f'Stack limit: soft={soft}, hard={hard}')
except ImportError:
print('resource module not available (Windows)')
"
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Python 3.14 (Oct 2025) | Current | PEP 651 may introduce StackOverflow/RecursionOverflow | Watch for new exception types |
| Python 3.13 (Oct 2024) | Supported | Py_C_RECURSION_LIMIT hardcoded; free-threaded build | C recursion limit cannot be changed at runtime |
| Python 3.12 (Oct 2023) | Supported | sys.setrecursionlimit only controls Python-level recursion | Code relying on limit for C recursion may break |
| Python 3.11 (Oct 2022) | Security fixes | None recursion-specific | -- |
| Python 3.5 (Sep 2015) | EOL | RecursionError introduced (was RuntimeError) | Use except RecursionError for specificity |
| Python 2.7 (Jul 2010) | EOL | Raises RuntimeError, no RecursionError class | Upgrade to Python 3.x |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Base case is missing or wrong | You need depth > 10,000 | Convert to iterative with explicit stack |
| Recursion depth is slightly over 1000 | Writing a performance-critical hot loop | Iterative version is faster (no call overhead) |
| Debugging mutual recursion between functions | Input size is unbounded (user-controlled) | Iterative -- recursion depth is a DoS vector |
| Prototyping and depth is known/bounded | Working with very deeply nested JSON/XML | Use iterative parser or json.loads limit (3.12+) |
@lru_cache can reduce effective depth | Deploying to production with unknown inputs | Iterative is always safer for production code |
sys.setrecursionlimit() and the C stack recursion limit are decoupled. Raising the Python limit does not prevent C-level stack overflow.RecursionError was introduced in Python 3.5. Code catching RuntimeError to handle recursion may inadvertently catch unrelated errors.re module uses C-level recursion for regex matching. Complex patterns on long strings can hit the C recursion limit independently.asyncio, recursive async functions consume coroutine frames that count toward the recursion limit.