Fix "RecursionError: maximum recursion depth exceeded" in Python

Fix "RecursionError: maximum recursion depth exceeded"

Python raises this the moment a chain of function calls stacks up past its default limit of 1000. A missing base case is the usual culprit, but the same traceback also shows up on recursive code that's actually written correctly, and on a dunder method that most developers never think of as "recursion" until it fails exactly like this. This article walks through a reproduction of each, and where sys.setrecursionlimit() genuinely helps versus where it just delays a worse crash.

Reproducing the Error: A Missing Base Case

The most common cause is also the simplest: a recursive function that calls itself but never actually reaches a condition that stops it.

def factorial(n):
    return n * factorial(n - 1)  # no base case: never checks for n == 0

factorial(5)
Traceback (most recent call last):
  File "factorial.py", line 4, in 
    factorial(5)
  File "factorial.py", line 2, in factorial
    return n * factorial(n - 1)
                ^^^^^^^^^^^^^^^^
  File "factorial.py", line 2, in factorial
    return n * factorial(n - 1)
                ^^^^^^^^^^^^^^^^
  [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded

n counts down past zero into negative numbers forever, since nothing in the function ever returns without calling itself again. The fix here has nothing to do with recursion limits: it's an ordinary missing-base-case bug.

def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

print(factorial(5))
# 120

Why Python Has a Recursion Limit at All

Every Python-level function call, recursive or not, consumes a real frame on the C call stack the interpreter itself is running on. Python tracks how deep that stack has gotten and raises RecursionError once it passes sys.getrecursionlimit() (1000 by default), deliberately, as a catchable exception. That check exists specifically to fail safely: if Python let the recursion keep going until the actual operating-system stack ran out, the interpreter process itself could crash with a segmentation fault instead of raising anything at all, which is far harder to debug and can't be caught by any try/except.

Note: this is also how to tell a stuck-forever recursive bug apart from a stuck-forever loop during debugging. A while True loop with no exit condition just hangs, silently, until something kills the process. Recursion fails fast and loud with this specific traceback, because it's bounded by stack depth, not by CPU time.

Legitimately Deep Recursion: When the Logic Is Correct but the Input Is Large

Not every case is a bug. A recursive function can be written with a perfectly correct base case and still hit the limit, simply because the input is bigger than 1000 items:

def sum_list_recursive(lst):
    if not lst:
        return 0
    return lst[0] + sum_list_recursive(lst[1:])

big_list = list(range(3000))
sum_list_recursive(big_list)
Traceback (most recent call last):
  File "sumlist.py", line 6, in 
    sum_list_recursive(big_list)
  File "sumlist.py", line 3, in sum_list_recursive
    return lst[0] + sum_list_recursive(lst[1:])
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^
  [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded in comparison

The base case (if not lst) is correct and will eventually be reached; the recursion is doing exactly what it's supposed to. It just needs one stack frame per element, and 3000 elements is three times the default limit.

The Right Fix: Rewrite as a Loop, Not a Higher Limit

sys.setrecursionlimit() can technically make the example above run:

import sys
sys.setrecursionlimit(5000)
sum_list_recursive(big_list)  # now succeeds, matches sum(big_list)
Why this isn't the recommended fix: the new ceiling still comes from the real operating-system stack, a resource Python doesn't control precisely, so pushing the number up just relocates the wall to somewhere less predictable. Feed a deeper input than the new limit anticipates and the interpreter can crash outright with a segmentation fault, a failure mode with no traceback and nothing to catch. An iterative version sidesteps the whole question, since a loop's stack usage stays flat no matter how many items it processes:
def sum_list_iterative(lst):
    total = 0
    for x in lst:
        total += x
    return total

print(sum_list_iterative(big_list) == sum(big_list))
# True, and no recursion limit involved at all

This is the same reason Python's own standard library avoids deep recursion in code that has to handle arbitrary-sized input, for example JSON parsing or tree traversal over untrusted data. If the recursion depth scales with something a caller controls (list length, tree depth, nesting level from a file), that's a sign to reach for a loop or an explicit stack (a plain list used as a stack, pushed and popped manually) rather than a permanently raised limit.

The Trap That Isn't Obviously Recursion: __getattr__

The least obvious version of this error doesn't involve a function calling itself directly at all. __getattr__ is a dunder method Python calls automatically, but only when normal attribute lookup already failed, which makes it dangerously easy to write a version that fails that same lookup again from inside itself:

class Config:
    def __init__(self, data):
        self._data = data

    def __getattr__(self, name):
        # bug: self.data (no underscore) isn't a real attribute either,
        # so accessing it triggers __getattr__ again
        return self.data.get(name)

c = Config({"x": 1})
c.x
Traceback (most recent call last):
  ...
  File "config.py", line 6, in __getattr__
    return self.data.get(name)
            ^^^^^^^^^
  File "config.py", line 6, in __getattr__
    return self.data.get(name)
            ^^^^^^^^^
  [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded

The constructor stores the dictionary on self._data, with an underscore, but __getattr__'s own body reads self.data, without one. Since data isn't a real attribute of the instance, looking it up fails normal attribute resolution too, which calls __getattr__ again to try to resolve data, which reads self.data again, and so on. The fix is making sure __getattr__ never references an attribute name that isn't guaranteed to already exist on the instance:

class ConfigFixed:
    def __init__(self, data):
        self._data = data

    def __getattr__(self, name):
        return self._data.get(name)  # matches the real attribute name

cf = ConfigFixed({"x": 1})
print(cf.x)
# 1

This same shape shows up with __getattr__ implementations that reference self.__dict__ indirectly through another attribute-style access, or that call getattr(self, other_name) on a name that also doesn't exist yet. Any path through __getattr__ that can lead back into a failed lookup on self reproduces the same infinite loop.

Summary: Which Fix Applies

Situation                                              Fix
──────────────────────────────────────────────────    ─────────────────────────────────────────
A recursive function never reaches its base case        Add or correct the base case; this is an
                                                          ordinary logic bug, not a limit problem
Correct recursion, input larger than the default limit   Rewrite as a loop or explicit stack;
                                                          only raise the limit for a known, small,
                                                          measured input ceiling
__getattr__ references an attribute that also            Make sure every name __getattr__ reads
doesn't exist                                            from self already exists as a real attribute