← Back to DevBytes

Fix 'RecursionError' in Python: Complete Troubleshooting Guide

Understanding RecursionError in Python

A RecursionError in Python is raised when the interpreter detects that the maximum recursion depth has been exceeded. By default, Python sets a limit of 1000 recursive calls to prevent infinite recursion from consuming all available memory and crashing the system. This limit is stored in sys.getrecursionlimit() and can be inspected or modified at runtime.

import sys
print(sys.getrecursionlimit())  # Output: 1000 (default)

When a function calls itself too many times without reaching a base case, the call stack grows until it hits this ceiling. At that point, Python throws:

RecursionError: maximum recursion depth exceeded

A variant also exists when a C function calls back into Python too many times:

RecursionError: maximum recursion depth exceeded while calling a Python object

Common Causes of RecursionError

The most frequent triggers fall into these categories:

Why Fixing RecursionError Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Ignoring or hastily patching a RecursionError leads to serious consequences:

Understanding how to properly diagnose and resolve recursion errors builds fundamental skills in algorithm design, stack management, and defensive programming.

How to Diagnose and Fix RecursionError

1. Identify the Recursive Pattern with Tracebacks

When a RecursionError occurs, Python prints a truncated traceback showing the last few hundred frames. The repetition of the same function names is your primary clue. Use traceback module to capture the full stack if needed:

import traceback
import sys

def recursive_function(n):
    if n == 0:
        return 0
    return recursive_function(n - 1)  # Missing base case for negative n

try:
    recursive_function(-1)
except RecursionError:
    # Print the last 10 frames to avoid overwhelming output
    tb = sys.exc_info()[2]
    for frame in traceback.extract_tb(tb)[-10:]:
        print(f"File {frame.filename}, line {frame.lineno}, in {frame.name}")
        print(f"    {frame.line}")

2. Fix Missing or Incorrect Base Cases

The most common fix: ensure every recursive path eventually reaches a termination condition. Here's a classic factorial function with a bug and its correction:

❌ Broken — no base case for negative input:

def factorial(n):
    return n * factorial(n - 1)  # RecursionError for n < 0

✅ Fixed — explicit base case and input validation:

def factorial(n):
    if n < 0:
        raise ValueError("Factorial is not defined for negative numbers")
    if n == 0 or n == 1:
        return 1
    return n * factorial(n - 1)

print(factorial(5))   # Output: 120
print(factorial(0))   # Output: 1
# factorial(-3)       # Raises ValueError cleanly

3. Convert Recursion to Iteration

For problems that can grow arbitrarily large (tree traversal, graph search, Fibonacci), an iterative solution avoids the recursion limit entirely and often performs better:

Recursive Fibonacci (prone to RecursionError for large n):

def fibonacci_recursive(n):
    if n <= 1:
        return n
    return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)

# fibonacci_recursive(1500)  # RecursionError

Iterative Fibonacci (safe for any n):

def fibonacci_iterative(n):
    if n <= 1:
        return n
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b

print(fibonacci_iterative(1500))  # Works fine (large number)

4. Use Tail Recursion Optimization (Manual)

Python does not automatically optimize tail recursion, but you can manually refactor tail-recursive functions into loops. A tail-recursive function has the recursive call as its final operation:

Tail-recursive sum (will hit RecursionError for large lists):

def sum_list_tail(lst, acc=0):
    if not lst:
        return acc
    return sum_list_tail(lst[1:], acc + lst[0])  # Tail call

Converted to iterative loop:

def sum_list_iterative(lst):
    acc = 0
    for item in lst:
        acc += item
    return acc

# Handles lists of any size
print(sum_list_iterative(range(1000000)))  # Output: 499999500000

5. Adjust the Recursion Limit (Use Sparingly)

When recursion is genuinely the clearest approach and you control the input size, you can raise the recursion limit. This is a last resort, not a fix for logic errors.

import sys

def deep_tree_search(node, target):
    if node.value == target:
        return node
    for child in node.children:
        result = deep_tree_search(child, target)
        if result:
            return result
    return None

# For known deep trees (e.g., 5000 levels)
sys.setrecursionlimit(10000)
result = deep_tree_search(root, target_value)
sys.setrecursionlimit(1000)  # Restore default after operation

Important caveats: Increasing the limit consumes more C stack memory. On some platforms, setting it too high can cause a segmentation fault rather than a clean Python exception. Always restore the limit or use a context manager:

from contextlib import contextmanager
import sys

@contextmanager
def recursion_limit(limit):
    old_limit = sys.getrecursionlimit()
    sys.setrecursionlimit(limit)
    try:
        yield
    finally:
        sys.setrecursionlimit(old_limit)

# Usage
with recursion_limit(5000):
    result = deep_tree_search(root, target)

6. Fix Recursion in Special Methods

RecursionError often lurks in __getattr__, __setattr__, __repr__, and __str__ when they inadvertently call themselves. Here's how to spot and fix them:

❌ Broken __getattr__ — infinite recursion:

class BrokenProxy:
    def __init__(self, data):
        self._data = data
    
    def __getattr__(self, name):
        # Forgetting to check for _data first causes infinite loop
        return getattr(self._data, name)

obj = BrokenProxy({"key": "value"})
print(obj.missing)  # RecursionError: __getattr__ keeps calling itself

✅ Fixed __getattr__ — use object.__getattribute__ for internal access:

class FixedProxy:
    def __init__(self, data):
        # Use object's method to set attribute directly
        object.__setattr__(self, '_data', data)
    
    def __getattr__(self, name):
        # __getattr__ is only called when normal lookup fails
        # Access _data safely
        data = object.__getattribute__(self, '_data')
        if name.startswith('__') and name.endswith('__'):
            raise AttributeError(name)
        return data.get(name, None)

obj = FixedProxy({"key": "value"})
print(obj.key)       # Output: value
print(obj.missing)   # Output: None (no recursion)

❌ Broken __repr__ — self-referencing structure:

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None
    
    def __repr__(self):
        return f"Node({self.value}, next={self.next})"

n1 = Node(1)
n2 = Node(2)
n1.next = n2
n2.next = n1  # Circular reference
print(n1)  # RecursionError

✅ Fixed __repr__ — limit depth or detect cycles:

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None
    
    def __repr__(self):
        return self._repr_with_depth(max_depth=5)
    
    def _repr_with_depth(self, depth=0, max_depth=10):
        if depth >= max_depth:
            return f"Node({self.value}, next=...)"
        if self.next is None:
            return f"Node({self.value}, next=None)"
        if isinstance(self.next, Node):
            return f"Node({self.value}, next={self.next._repr_with_depth(depth + 1, max_depth)})"
        return f"Node({self.value}, next={self.next!r})"

n1 = Node(1)
n2 = Node(2)
n1.next = n2
n2.next = n1
print(n1)  # Output: Node(1, next=Node(2, next=Node(1, next=Node(2, next=Node(1, next=...)))))

7. Handle Mutual Recursion

When two functions call each other, missing base cases in either one causes a RecursionError. Track depth explicitly:

def is_even(n, depth=0, max_depth=1000):
    if depth > max_depth:
        raise RecursionError("Mutual recursion depth exceeded")
    if n == 0:
        return True
    return is_odd(abs(n) - 1, depth + 1, max_depth)

def is_odd(n, depth=0, max_depth=1000):
    if depth > max_depth:
        raise RecursionError("Mutual recursion depth exceeded")
    if n == 0:
        return False
    return is_even(abs(n) - 1, depth + 1, max_depth)

print(is_even(42))   # Output: True
print(is_odd(42))    # Output: False
# is_even(-1)         # Raises controlled RecursionError with clear message

8. Use Memoization to Reduce Recursion Depth

For problems with overlapping subproblems (dynamic programming), memoization drastically reduces the number of recursive calls:

from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci_memoized(n):
    if n < 0:
        raise ValueError("Negative argument")
    if n <= 1:
        return n
    return fibonacci_memoized(n - 1) + fibonacci_memoized(n - 2)

# Now works for reasonably large n without hitting recursion limit
print(fibonacci_memoized(500))  
# Output: 13942322456169788013972438287040728395007025658769730726410896694832557114786369...

9. Implement Stack Simulation (Manual Stack)

For algorithms inherently recursive like depth-first search, simulate the call stack with an explicit stack data structure. This gives you full control and unlimited "depth":

def dfs_iterative(root):
    """Depth-first search without recursion."""
    if root is None:
        return []
    
    result = []
    stack = [root]
    visited = set()
    
    while stack:
        node = stack.pop()
        if node not in visited:
            visited.add(node)
            result.append(node.value)
            # Push children in reverse order for left-to-right traversal
            for child in reversed(node.children):
                if child is not None:
                    stack.append(child)
    
    return result

# Works on trees of any depth without RecursionError

Best Practices to Prevent RecursionError

Debugging Checklist

When facing a RecursionError, work through this checklist in order:

  1. Print the traceback — identify the repeating function name(s)
  2. Check base cases — are they reachable for all possible inputs?
  3. Add a depth counter — insert a depth parameter and print it to see how deep you're going before the crash
  4. Test edge cases — negative numbers, empty strings, zero-length collections, None
  5. Look for mutual recursion — search for pairs of functions calling each other
  6. Inspect special methods — review __getattr__, __setattr__, __repr__, __str__ for self-calls
  7. Consider iteration — can the algorithm be rewritten with a loop and an explicit stack?
  8. Apply memoization — will caching eliminate redundant branches?
  9. Only as a last resort — carefully raise the recursion limit with a context manager

Complete Troubleshooting Example: File System Walker

Below is a realistic scenario: a recursive function that walks a file system tree. It demonstrates the problem, diagnosis, and multiple fixes:

❌ Version that fails on deep directory structures:

import os

def count_files_recursive(path):
    """Count all files recursively — fails on deep trees."""
    total = 0
    for entry in os.listdir(path):
        full_path = os.path.join(path, entry)
        if os.path.isfile(full_path):
            total += 1
        elif os.path.isdir(full_path):
            total += count_files_recursive(full_path)  # RecursionError possible
    return total

# count_files_recursive('/very/deep/path')  # May raise RecursionError

✅ Fixed version with iterative stack:

import os

def count_files_iterative(root_path):
    """Count files using an explicit stack — no recursion limit."""
    total = 0
    stack = [root_path]
    
    while stack:
        current_path = stack.pop()
        try:
            entries = os.listdir(current_path)
        except PermissionError:
            continue  # Skip directories we can't access
        
        for entry in entries:
            full_path = os.path.join(current_path, entry)
            try:
                if os.path.isfile(full_path):
                    total += 1
                elif os.path.isdir(full_path):
                    stack.append(full_path)
            except OSError:
                continue  # Handle broken symlinks, etc.
    
    return total

print(count_files_iterative('.'))  # Works regardless of depth

✅ Alternative: hybrid approach with controlled recursion limit and depth tracking:

import os
import sys
from contextlib import contextmanager

@contextmanager
def temporary_recursion_limit(limit):
    old = sys.getrecursionlimit()
    sys.setrecursionlimit(limit)
    try:
        yield
    finally:
        sys.setrecursionlimit(old)

def count_files_hybrid(path, depth=0, max_depth=900):
    """Recursive with depth guard and temporary limit adjustment."""
    if depth > max_depth:
        # Switch to iterative mode for remaining subtree
        return count_files_iterative(path)
    
    total = 0
    try:
        entries = os.listdir(path)
    except PermissionError:
        return 0
    
    for entry in entries:
        full_path = os.path.join(path, entry)
        try:
            if os.path.isfile(full_path):
                total += 1
            elif os.path.isdir(full_path):
                total += count_files_hybrid(full_path, depth + 1, max_depth)
        except OSError:
            continue
    
    return total

# Usage
with temporary_recursion_limit(2000):
    result = count_files_hybrid('/some/deep/tree')
    print(f"Total files: {result}")

Conclusion

RecursionError is Python's safeguard against runaway recursion consuming system resources. The fix is rarely about increasing the recursion limit — it's about understanding why the recursion isn't terminating or why a recursive approach is inappropriate for the problem's scale. By methodically checking base cases, validating inputs, converting to iteration when necessary, and applying memoization or explicit stacks, you can eliminate recursion errors while keeping your code clean and efficient. The debugging checklist and practical patterns in this guide give you a systematic approach to diagnose and resolve every occurrence of RecursionError in your Python projects. Remember: the best fix is often a well-placed base case, not a higher number in sys.setrecursionlimit().

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles