__getattr__ RecursionError: maximum recursion depth exceeded (tested)
A __getattr__ that forwards to self.data works until something builds the object without __init__: then it hits RecursionError: maximum recursion depth exceeded. We measured this with copy and pickle on Python 3.13.5, and the fix that handles both.
1. A __getattr__ that looks fine
class Config:
def __init__(self, data):
self.data = data
def __getattr__(self, name):
return self.data[name]
c = Config({"port": 80})
print(c.port)
for label, fn in [("c.missing", lambda: c.missing), ("hasattr(c, 'missing')", lambda: hasattr(c, "missing"))]:
try:
print(label, "->", fn())
except Exception as e:
print(label, "->", type(e).__name__ + ":", e)
Output:
80
c.missing -> KeyError: 'missing'
hasattr(c, 'missing') -> KeyError: 'missing'
Measured on Python 3.13.5: normal use works, but it already has a bug: a missing key raises KeyError, so hasattr() raises instead of returning False. __getattr__ must raise AttributeError.
2. copy and pickle make it recurse forever
import copy, pickle
class Config:
def __init__(self, data):
self.data = data
def __getattr__(self, name):
return self.data[name]
c = Config({"port": 80})
for label, fn in [("copy.copy", copy.copy), ("pickle round-trip", lambda o: pickle.loads(pickle.dumps(o)))]:
try:
fn(c)
print(label, "ok")
except RecursionError as e:
print(label, "-> RecursionError:", e)
Output:
copy.copy -> RecursionError: maximum recursion depth exceeded
pickle round-trip -> RecursionError: maximum recursion depth exceeded
Measured: both hit RecursionError: maximum recursion depth exceeded. copy and pickle create the new object without calling __init__, so self.data doesn't exist yet. Looking up a missing attribute calls __getattr__('data'), which reads self.data, which calls __getattr__('data')… We confirmed it directly: Config.__new__(Config).port raises the same RecursionError. Any code path that touches an attribute before __init__ sets it (an exception in __init__, a __new__ override, a mock) does the same.
3. Fix: read the backing dict directly, raise AttributeError, skip dunders
import copy, pickle
class Config:
def __init__(self, data):
self.data = data
def __getattr__(self, name):
if name.startswith("__"):
raise AttributeError(name)
try:
return self.__dict__["data"][name]
except KeyError:
raise AttributeError(name) from None
c = Config({"port": 80})
print(c.port, getattr(c, "missing", "default"), hasattr(c, "missing"))
print(copy.copy(c).port, pickle.loads(pickle.dumps(c)).port)
Output:
80 default False
80 80
self.__dict__ is found by normal lookup and never goes through __getattr__, so a missing data becomes a KeyError converted to AttributeError, not recursion. Refusing __dunder__ names keeps copy, pickle and other protocol probes on their default paths. Measured: getattr with a default, hasattr, copy and pickle all work.
4. __getattribute__ recurses on every access
calls = 0
class Wrapper:
def __init__(self):
object.__setattr__(self, "x", 1)
def __getattribute__(self, name):
global calls
calls += 1
return self.__dict__[name] # self.__dict__ calls __getattribute__ again
class Fixed(Wrapper):
def __getattribute__(self, name):
return object.__getattribute__(self, name)
try:
Wrapper().x
except RecursionError as e:
print("RecursionError:", e, "| calls:", calls)
print("Fixed:", Fixed().x)
Output:
RecursionError: maximum recursion depth exceeded | calls: 999
Fixed: 1
Measured: unlike __getattr__ (called only when normal lookup fails), __getattribute__ runs for every attribute access, including self.__dict__ inside itself, so it recursed 999 times before failing. Inside __getattribute__, always go through object.__getattribute__(self, name).
Frequently asked questions
Why does my __getattr__ cause RecursionError?
It reads an attribute (like self.data) that doesn't exist yet, so that read calls __getattr__ again. We measured this happening on copy.copy and pickle, which create the object without running __init__.
How do I avoid recursion in __getattr__?
Read backing state via self.__dict__['data'], raise AttributeError for missing names, and refuse dunder names. We measured copy, pickle, getattr and hasattr all working with that version.
Should __getattr__ raise KeyError or AttributeError?
AttributeError. We measured a KeyError-raising __getattr__ making hasattr() raise KeyError instead of returning False.
Why does __getattribute__ recurse with self.__dict__?
__getattribute__ handles every attribute access, including self.__dict__ inside it. Use object.__getattribute__(self, name); we measured the naive version recursing 999 times.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5 on Linux: a dict-backed __getattr__ under normal use, hasattr, copy.copy and a pickle round-trip, the fixed version, and a naive vs object.__getattribute__-based __getattribute__. Every block was run verbatim and its output pasted from that run. PlainHeaders is our extension; everything else here is built-in Python.