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.
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
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.
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.
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.
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)
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 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.
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