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