- __getattr__ only fires when normal attribute lookup fails (missing attributes), while __getattribute__ intercepts every single access — mixing them up causes silent bugs.
- __getattribute__ adds 87% overhead compared to normal attribute access (benchmarked on Python 3.11), so reserve it for debugging and use __getattr__ for lazy loading and proxies.
- Always use object.__getattribute__(self, name) inside custom __getattribute__ to avoid infinite recursion, and skip internal attributes like __dict__ to prevent pickle breakage.
The Silent Failure That Took 20 Minutes to Debug
A lazy-loading config object was silently returning None for every attribute that didn’t exist. No errors, no warnings — just quiet failure that propagated through the entire pipeline until a JSON serialization crash three layers deep revealed the problem.
The culprit? Confusing __getattr__ with __getattribute__.
Python gives you two hooks for intercepting attribute access, and they behave radically differently. One only fires when normal lookup fails. The other intercepts every single access, including methods you didn’t even know your class was using. Mix them up and you get subtle bugs that only surface in production.
Here’s what happens when you run this:
class LazyConfig:
def __getattribute__(self, name):
print(f"__getattribute__: {name}")
return object.__getattribute__(self, name)
def __getattr__(self, name):
print(f"__getattr__: {name}")
return None
config = LazyConfig()
config.database_url # What prints?
Most developers expect __getattr__ to fire. But both methods print — and in a specific order that reveals Python’s attribute lookup chain.

When Each Hook Fires (and Why It Matters)
__getattribute__ is the first responder. Python calls it for every attribute access, whether the attribute exists or not. It’s the lookup mechanism itself.
__getattr__ is the fallback. It only fires when __getattribute__ raises AttributeError — meaning normal lookup failed.
The execution flow looks like this:
\text{access } x.foo \rightarrow \text{__getattribute__}("foo") \rightarrow \begin{cases}
\text{return value} & \text{if found} \\
\text{raise AttributeError} & \text{if not found}
\end{cases} \rightarrow \text{__getattr__}("foo")
Here’s a minimal demonstration:
class ProxyLogger:
def __init__(self):
self.data = {"status": "ready"}
def __getattribute__(self, name):
print(f"[__getattribute__] Looking up: {name}")
return object.__getattribute__(self, name)
def __getattr__(self, name):
print(f"[__getattr__] Fallback for: {name}")
return f"missing_{name}"
proxy = ProxyLogger()
print(proxy.data) # Exists — only __getattribute__ fires
print(proxy.missing) # Missing — both fire
Output:
[__getattribute__] Looking up: data
{'status': 'ready'}
[__getattribute__] Looking up: missing
[__getattr__] Fallback for: missing
missing_missing
Notice that __getattribute__ fires for data even though it exists. This is the key difference.
Pattern 1: Lazy Attribute Loading (Use getattr)
The most common use case is lazy initialization — compute or fetch values only when accessed.
import time
class LazyDatabase:
def __init__(self, config_path):
self.config_path = config_path
self._connection = None # Not loaded yet
def __getattr__(self, name):
# Only called when attribute doesn't exist
if name == "connection":
print("Initializing database connection...")
time.sleep(0.5) # Simulate connection time
self._connection = f"DB({self.config_path})"
return self._connection
raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
db = LazyDatabase("/etc/db.conf")
print("Database object created") # No connection yet
print(db.connection) # Triggers __getattr__ once
print(db.connection) # Uses cached _connection — __getattr__ doesn't fire
Output:
Database object created
Initializing database connection...
DB(/etc/db.conf)
DB(/etc/db.conf)
The second access hits _connection directly because it now exists in __dict__. __getattr__ never fires again.
But watch what happens if you try to implement caching with __getattribute__:
class BrokenLazyDB:
def __init__(self, config_path):
self.config_path = config_path
self._connection = None
def __getattribute__(self, name):
if name == "connection":
conn = object.__getattribute__(self, "_connection")
if conn is None:
print("Initializing...")
time.sleep(0.5)
# This line calls __setattr__, which you'd also need to override
object.__setattr__(self, "_connection", "DB_CONN")
return "DB_CONN"
return conn
return object.__getattribute__(self, name)
db = BrokenLazyDB("/etc/db.conf")
print(db.connection) # Works
print(db.config_path) # Also works, but...
This works, but it’s verbose and fragile. Every access to connection — even after initialization — goes through the if name == "connection" check. You’re intercepting all attribute access just to handle one special case.
Use __getattr__ for lazy loading. Reserve __getattribute__ for when you genuinely need to intercept everything.
Pattern 2: Dynamic Proxy Objects (Use getattr)
Wrapping APIs or forwarding method calls to a delegate:
class APIClient:
def get_user(self, user_id):
return f"User {user_id}"
def get_posts(self, user_id, limit=10):
return f"Posts for {user_id} (limit={limit})"
class RateLimitedProxy:
def __init__(self, client, max_calls=100):
self._client = client
self._calls = 0
self._max_calls = max_calls
def __getattr__(self, name):
# Forward attribute access to wrapped client
attr = getattr(self._client, name)
if callable(attr):
def rate_limited_call(*args, **kwargs):
if self._calls >= self._max_calls:
raise Exception("Rate limit exceeded")
self._calls += 1
return attr(*args, **kwargs)
return rate_limited_call
return attr
api = APIClient()
proxy = RateLimitedProxy(api, max_calls=2)
print(proxy.get_user(123)) # Works
print(proxy.get_posts(123, 5)) # Works
print(proxy.get_user(456)) # Raises rate limit exception
The magic here: proxy.get_user(123) triggers __getattr__("get_user"), which wraps the underlying method with rate limiting logic. You never had to explicitly define get_user on the proxy.
This pattern powers libraries like Boto3 for AWS — dynamically generated clients that forward calls to API endpoints without hardcoding every single method.
Pattern 3: Access Logging and Debugging (Use getattribute)
Now we hit the rare case where you do want to intercept everything.
import json
class AccessLogger:
def __init__(self, data):
object.__setattr__(self, "_data", data)
object.__setattr__(self, "_log", [])
def __getattribute__(self, name):
# Skip internal attributes to avoid recursion
if name.startswith("_") or name == "get_log":
return object.__getattribute__(self, name)
log = object.__getattribute__(self, "_log")
log.append(f"read: {name}")
data = object.__getattribute__(self, "_data")
return data.get(name, f"missing_{name}")
def __setattr__(self, name, value):
if name.startswith("_"):
object.__setattr__(self, name, value)
return
log = object.__getattribute__(self, "_log")
log.append(f"write: {name} = {value}")
data = object.__getattribute__(self, "_data")
data[name] = value
def get_log(self):
return self._log
config = AccessLogger({"timeout": 30, "retries": 3})
print(config.timeout) # Read existing
config.max_workers = 8 # Write new
print(config.retries) # Read existing
print(config.get_log())
Output:
30
3
['read: timeout', 'write: max_workers = 8', 'read: retries']
Notice the recursion trap: if you call self._log.append(...) inside __getattribute__, Python intercepts _log access, which triggers __getattribute__ again, which accesses _log, which triggers… infinite loop.
The fix: always use object.__getattribute__(self, "_log") for internal state. This bypasses your custom hook.
Same applies to __setattr__ — use object.__setattr__(self, name, value) to actually store data without recursion.

The Infinite Recursion Landmine
This crashes:
class Broken:
def __getattribute__(self, name):
print(f"Accessing {name}")
return self.name # RECURSION: accesses self.name → __getattribute__ → self.name → ...
Fix it with object.__getattribute__:
class Fixed:
def __getattribute__(self, name):
print(f"Accessing {name}")
return object.__getattribute__(self, name) # Bypass custom hook
This is the #1 mistake when writing __getattribute__. You must delegate to object.__getattribute__ for the actual lookup.
Performance: The Hidden Cost of Universal Interception
Every attribute access in Python triggers the descriptor protocol, which involves checking __dict__, __slots__, the MRO (method resolution order), and finally your custom hooks. Overriding __getattribute__ adds overhead to every single access.
Here’s a benchmark:
import timeit
class NormalClass:
def __init__(self):
self.x = 1
self.y = 2
class WithGetAttr:
def __init__(self):
self.x = 1
self.y = 2
def __getattr__(self, name):
raise AttributeError(name)
class WithGetAttribute:
def __init__(self):
self.x = 1
self.y = 2
def __getattribute__(self, name):
return object.__getattribute__(self, name)
normal = NormalClass()
with_getattr = WithGetAttr()
with_getattribute = WithGetAttribute()
print("Normal:", timeit.timeit("obj.x", globals={"obj": normal}, number=1_000_000))
print("__getattr__:", timeit.timeit("obj.x", globals={"obj": with_getattr}, number=1_000_000))
print("__getattribute__:", timeit.timeit("obj.x", globals={"obj": with_getattribute}, number=1_000_000))
On Python 3.11 (M1 MacBook):
Normal: 0.031s
__getattr__: 0.032s
__getattribute__: 0.058s
__getattribute__ is ~87% slower than normal access. __getattr__ has negligible overhead because it only fires on AttributeError.
If you’re building a high-performance data structure accessed in tight loops, avoid __getattribute__. Use __getattr__ or rethink your design.
Combining Both (Rarely Needed, But Possible)
You can define both hooks in the same class:
class Hybrid:
def __init__(self):
self.real_attr = "exists"
def __getattribute__(self, name):
print(f"[__getattribute__] {name}")
try:
return object.__getattribute__(self, name)
except AttributeError:
print(f"[__getattribute__] Not found, raising AttributeError")
raise # This triggers __getattr__
def __getattr__(self, name):
print(f"[__getattr__] {name}")
return f"fallback_{name}"
h = Hybrid()
print(h.real_attr)
print(h.missing_attr)
Output:
[__getattribute__] real_attr
exists
[__getattribute__] missing_attr
[__getattribute__] Not found, raising AttributeError
[__getattr__] missing_attr
fallback_missing_attr
But honestly? If you need both, your design is probably too complex. Pick one.
Real-World Gotcha: Pickle and Copy Compatibility
Objects with __getattribute__ often break pickle and copy.deepcopy because Python’s serialization mechanism relies on direct __dict__ access.
import pickle
class LoggedAccess:
def __init__(self, value):
self.value = value
def __getattribute__(self, name):
print(f"Access: {name}")
return object.__getattribute__(self, name)
obj = LoggedAccess(42)
pickle.dumps(obj) # Works, but prints "Access: __getstate__", "Access: __dict__", etc.
You’ll see debug logs for every internal attribute Python’s pickle module touches. If your __getattribute__ has side effects (like writing to a database), this can cause unexpected behavior.
The fix: explicitly skip pickling-related attributes:
def __getattribute__(self, name):
if name in ("__dict__", "__getstate__", "__setstate__"):
return object.__getattribute__(self, name)
# Your custom logic here
return object.__getattribute__(self, name)
Or better yet, just use __getattr__ unless you genuinely need universal interception.
When to Use Which
Use __getattr__ when:
– Lazy loading attributes (compute on first access)
– Proxying/delegating to another object
– Providing defaults for missing attributes
– Implementing dynamic APIs (like ORM query builders)
Use __getattribute__ when:
– Logging or auditing every attribute access
– Enforcing access control on all attributes
– Debugging attribute lookup issues
– Building low-level instrumentation tools
Never use __getattribute__ when:
– You only care about missing attributes (use __getattr__)
– Performance matters (87% overhead is real)
– You’re not absolutely sure you need it
The vast majority of use cases fall into the first category. I’ve written production Python for years and used __getattribute__ maybe twice — both for debugging tools that logged every single access during a trace session.
FAQ
Q: Can I call super().__getattr__() in a subclass?
Only if the parent class defines __getattr__. Unlike __init__, __getattr__ isn’t guaranteed to exist in the inheritance chain. If you call super().__getattr__(name) and the parent doesn’t define it, you’ll get AttributeError. Better to wrap it in a try-except or check with hasattr(super(), "__getattr__").
Q: Why does my __getattribute__ still raise AttributeError even though I defined it?
You’re probably calling object.__getattribute__(self, name) for an attribute that doesn’t exist. That method always raises AttributeError if the attribute isn’t found — it doesn’t magically know about your fallback logic. You need to catch that exception and return a default, or let it propagate to trigger __getattr__.
Q: Does __getattr__ work with @property?
No. Properties are implemented as descriptors, which are resolved during __getattribute__ before __getattr__ even has a chance to fire. If you define @property foo, accessing obj.foo will always hit the property getter — __getattr__ never sees it.
The Debugging Checklist
Next time you’re tempted to override attribute access, run through this:
- Do I only care about missing attributes? → Use
__getattr__ - Do I need to intercept every access, including methods? → Use
__getattribute__(rare) - Am I calling
object.__getattribute__inside__getattribute__to avoid recursion? → Yes, always - Am I skipping internal attributes (like
_cacheor__dict__) from interception logic? → Yes, or you’ll recurse - Have I benchmarked the performance impact? → If it’s in a hot path, reconsider
Most bugs come from mixing up #1 and #2. __getattr__ is the lazy fallback. __getattribute__ is the nuclear option.
I’d bet good money that 95% of custom attribute access code should use __getattr__, but half of it mistakenly uses __getattribute__ because the name sounds like “get this attribute.” It’s not. It’s “intercept every single attribute access, including the ones you didn’t know existed.”
Pick the right tool. Your future self (and anyone debugging your code at 2am) will thank you. If you need caffeine to debug this stuff late at night, Dark Chocolate Espresso Beans are way better than another cup of stale coffee.
One thing I still don’t fully understand: why Python didn’t name these more intuitively. __getattr_missing__ and __getattr_all__ would have saved countless hours of confusion. But here we are.
Did you find this helpful?
Your support keeps this blog running and ad-free content coming.
☕ Buy me a coffeeMost Popular Posts
- Custom Metaclass in Python: 43% Faster Validation (12,977 views)
- yfinance Alternatives 2026: 7 Free APIs Compared (1,086 views)
- Python match-case: 7 Patterns That Beat if-elif Chains (996 views)
- YOLOv8 INT8 Quantization: 4x Faster on Jetson Orin (910 views)
- PaddleOCR vs EasyOCR vs Tesseract: Why PaddleOCR Is Slower (736 views)