Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

This error means Python kept nesting calls (or other call-like operations) until it reached its recursion safety limit. The repeated call is not always an obvious function calling itself: properties, attribute hooks, decorators, callbacks, special methods, and cyclic data can create the same loop. The reliable fix is to find that cycle, make execution progress toward a stopping condition, or replace recursion with iteration. Increasing the limit is only appropriate for known, finite, unusually deep recursion.

What the message means

RecursionError is a RuntimeError raised when the interpreter detects that the maximum recursion depth has been exceeded. Python keeps this guard partly to prevent uncontrolled Python calls from exhausting the underlying C stack and crashing the process. See the official exception documentation.

The suffix “while calling a Python object” is context from CPython’s call machinery. It tells you where recursion was noticed, not which line originally created the loop. A function may be calling itself indirectly, or an operation such as obj(...), obj.attr, repr(obj), or len(obj) may invoke user-defined code repeatedly.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Start with the smallest example

def f():
    f()

f()

This function never changes state or moves toward termination. A valid recursive function needs a reachable base case, a recursive step, and measurable progress:

def countdown(n):
    if n <= 0:             # base case
        return
    print(n)
    countdown(n - 1)      # progress toward the base case

countdown(3)

A base case that can never be reached is effectively no base case. If there are multiple recursive branches, each branch must eventually terminate.

Read the traceback as a call cycle

  1. Scroll to the bottom and confirm the final exception.
  2. Examine the repeated frames immediately above it.
  3. Look for the same line repeating, or for two or more functions alternating.
  4. Find the first transition that enters the repeating cycle; that is often where the missing condition lives.
File "example.py", line 4, in first
    second()
File "example.py", line 8, in second
    first()
File "example.py", line 4, in first
    second()
...
RecursionError: maximum recursion depth exceeded while calling a Python object

The traceback can be truncated, so do not focus only on its final repeated line. Draw the call chain and identify the smallest cycle. A temporary depth guard can expose the failing input earlier:

def walk(node, depth=0):
    if depth > 100:
        raise RuntimeError("unexpected recursion depth")
    # ...

For controlled logging, avoid formatting an object that may have a recursive __repr__ or __str__:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
print("type=", type(node).__name__, "id=", id(node))

Using repr(node) or an f-string containing the whole object can trigger the same bug you are trying to diagnose.

Common hidden causes

1. Mutual recursion

def parse(value):
    return validate(value)

def validate(value):
    return parse(value)

No function visibly calls itself, but the pair has no terminating state. Decide which function owns the stopping condition and add a decreasing measure, a state transition, or a result that breaks the cycle.

2. A property calls itself

class User:
    @property
    def name(self):
        return self.name          # invokes the getter again

    @name.setter
    def name(self, value):
        self.name = value          # invokes the setter again

Store the value in a separate backing attribute:

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

The leading underscore is a convention, not a security boundary. Property and descriptor behavior is described in Python’s descriptor HOWTO.

3. Recursive attribute hooks

Inside __getattribute__, ordinary attribute access goes through __getattribute__ again:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
class Config:
    def __getattribute__(self, name):
        return self.settings[name]   # recursion

Bypass the override for internal access:

class Config:
    def __getattribute__(self, name):
        if name == "settings":
            return object.__getattribute__(self, name)

        settings = object.__getattribute__(self, "settings")
        if name in settings:
            return settings[name]
        return object.__getattribute__(self, name)

A similar mistake with __getattr__ is asking for the same missing attribute:

class Settings:
    def __getattr__(self, name):
        return getattr(self, name)   # asks for the missing name again

Use a different storage location or raise AttributeError. See the attribute-access documentation.

4. Recursive __repr__, __str__, or logging

class Node:
    def __repr__(self):
        return f"Node({self})"

Formatting self invokes representation methods again. Parent/child objects that refer to each other can also recurse while being displayed. Prefer a cycle-safe representation:

class Node:
    def __repr__(self):
        return f"Node(value={self.value!r}, id={id(self)})"

print(obj), f-strings, exception formatting, container display, and logging may implicitly call __str__ or __repr__. During diagnosis, print only the type and identity. Python documents these hooks under __repr__ and __str__.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

5. A callable object or wrapper calls itself

class Repeater:
    def __call__(self, value):
        return self(value)          # calls the same instance

Call the intended operation instead:

class Doubler:
    def __call__(self, value):
        return value * 2

Decorator wrappers have the same trap:

def log_calls(func):
    def wrapper(*args, **kwargs):
        print("calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper

Returning wrapper(*args, **kwargs) from inside that wrapper recurses. Keep a reference to the original function.

6. Callbacks and observers re-trigger themselves

A GUI callback, retry hook, signal handler, ORM observer, or property notification can synchronously trigger the event that invoked it:

def callback():
    trigger(callback)

def trigger(fn):
    fn()

Check whether a callback changes the state that caused the callback, whether a setter notifies an observer that writes the same property, or whether a retry path has a bounded attempt count.

7. Cyclic graphs mistaken for trees

A traversal that assumes a tree will not terminate on a graph such as A → B → C → A:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
def walk(node, seen=None):
    if seen is None:
        seen = set()

    marker = id(node)
    if marker in seen:
        return
    seen.add(marker)

    for child in node.children:
        walk(child, seen)

Use id(node) when object identity is the intended criterion; a stable application-level node ID may be clearer. A visited set also avoids repeated shared subtrees, but that is a performance choice rather than proof of a cycle.

8. Overloaded operators and conversions

Inspect methods such as __eq__, __lt__, __iter__, __len__, __bool__, __hash__, and serializers. For example:

class Value:
    def __eq__(self, other):
        return self == other       # invokes __eq__ again

Likewise, __iter__ should return an iterator over internal data, not iter(self); __bool__ should not test if self.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Check the recursion limit—do not confuse it with the fix

import sys
print(sys.getrecursionlimit())

sys.getrecursionlimit() reports the current interpreter’s limit; a value around 1,000 is common in CPython, but it is not universal. The limit is distinct from your algorithm’s required depth and from the operating system’s underlying C-stack capacity.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

You can temporarily change it:

import sys
sys.setrecursionlimit(3000)

Do this only when the recursion is finite, bounded, intentional, and tested on the deployment platform. The Python documentation warns that an excessively high value can crash the interpreter; setting a value below the current depth raises RecursionError. Raising the limit cannot make an infinite cycle terminate and may merely delay diagnosis.

When iteration is safer

For deep linear work, an explicit loop avoids consuming one Python frame per step:

def factorial(n):
    result = 1
    for value in range(2, n + 1):
        result *= value
    return result

For a tree or graph, use an explicit stack while retaining cycle detection and traversal order:

def walk(root):
    stack = [root]
    seen = set()

    while stack:
        node = stack.pop()
        marker = id(node)
        if marker in seen:
            continue
        seen.add(marker)
        stack.extend(reversed(node.children))

Recursion remains clear for naturally hierarchical algorithms, divide-and-conquer code, and parsers whose depth is small and provably bounded. Python does not generally eliminate recursive frames through tail-call optimization.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A practical repair checklist

  • Confirm the final exception and inspect repeated traceback frames.
  • Look for direct self-calls and alternating function or method calls.
  • Verify that every recursive path makes progress toward a reachable base case.
  • Check property backing fields and attribute hooks.
  • Temporarily disable decorators, callbacks, observers, and overloaded methods.
  • Avoid printing suspect objects; log type names and id() values.
  • Add a visited set for graph-like data and a temporary depth guard.
  • Minimize the input to the smallest reproducer.
  • Prefer an explicit loop or stack for very deep input.
  • Only then consider a carefully tested recursion-limit increase.

If a third-party library is responsible, create a minimal reproducer, record the Python implementation and platform, and inspect the library’s version-specific issue tracker or patch the underlying cycle rather than masking it with a larger limit.

Quick decision tree

Does one function repeat in the traceback?
  Yes → Check its base case and progress.
  No → Do functions alternate?
          Yes → Break the mutual cycle.
          No → Inspect properties, descriptors, decorators,
               callbacks, and special methods.

Is the input finite but unusually deep?
  Yes → Prefer iteration; cautiously consider a higher limit.
Is the input cyclic?
  Yes → Track visited objects.

An import cycle is a different problem: it more often produces ImportError, partially initialized modules, or missing attributes than this exact recursion exception. First establish that the traceback really contains repeated Python calls.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.