`__getattr__` vs `__getattribute__` in Python
Learn the crucial difference between Python's `__getattr__` and `__getattribute__` methods, when to use each, and how to avoid common recursion traps with practical examples from production code.
Python's __getattr__ vs __getattribute__: The Difference That Can Make or Break Your Code
When I first started working with Python's attribute access methods, I'll admit I was confused. Both __getattr__ and __getattribute__ seem to do similar things - they intercept attribute lookups on objects. But the difference between them is crucial, and mixing them up can lead to some frustrating bugs.
Let me break this down in a way that actually makes sense.
The Core Difference
Think of __getattribute__ as the default route every attribute lookup takes. It runs every single time you access any attribute on an object. __getattr__, on the other hand, is the emergency backup - it only runs when normal attribute lookup fails.
Here's the simplest way to remember it:
- __getattribute__ runs always (even if the attribute exists)
- __getattr__ runs only when the attribute doesn't exist
Seeing It in Action
Let me show you a real example from PythonSkillset's debugging toolkit:
class DebugLogger:
def __init__(self):
self.log_count = 0
def __getattribute__(self, name):
print(f"Accessing: {name}")
return object.__getattribute__(self, name)
def __getattr__(self, name):
print(f"Attribute {name} not found - creating default")
return "default_value"
logger = DebugLogger()
print(logger.log_count) # This calls __getattribute__
print(logger.nonexistent) # This calls __getattribute__ first, then __getattr__
When you run this, you'll see that __getattribute__ fires for both accesses, but __getattr__ only kicks in for the second one.
Why This Matters in Practice
The practical implications are huge. Here's a scenario from PythonSkillset's configuration management system:
class ConfigWithDefaults:
def __init__(self):
self._config = {"host": "localhost", "port": 8080}
def __getattr__(self, name):
# Only runs when normal lookup fails
if name.startswith("_"):
raise AttributeError(name)
return self._config.get(name, "default_value")
config = ConfigWithDefaults()
print(config.host) # Hmm, this might cause recursion!
Notice the problem? If host doesn't exist as a direct attribute, __getattr__ runs, tries to access self._config, which calls __getattr__ again for _config... and boom, infinite recursion.
The Safe Way to Use These Methods
In PythonSkillset's production code, we always use this pattern:
class SafeConfig:
def __init__(self):
self._config = {"host": "localhost", "port": 8080}
def __getattr__(self, name):
# Use object.__getattribute__ for internal attributes
if name.startswith("_"):
raise AttributeError(f"{type(self).__name__} has no attribute {name}")
try:
return self._config[name]
except KeyError:
return "default_value"
def __getattribute__(self, name):
# Log all attribute access for debugging
print(f"Accessing {name}")
return object.__getattribute__(self, name)
When to Use Each One
Based on PythonSkillset's experience, here's my practical guidance:
Use __getattribute__ when:
- You need to intercept every attribute access (logging, profiling)
- You're building a proxy or wrapper pattern
- You want to control access to existing attributes
Use __getattr__ when:
- You're implementing a fallback or default mechanism
- You want to dynamically create attributes
- You're working with configuration or settings that have defaults
The Recursion Trap
This is the most common mistake I see. Consider this broken example:
# DON'T DO THIS
class Broken:
def __getattr__(self, name):
return self.something # Recursion!
Every time __getattr__ tries to access self.something, it triggers itself again. Always use object.__getattribute__ for internal attribute access:
# DO THIS INSTEAD
class Working:
def __getattr__(self, name):
# Safe internal access
internal = object.__getattribute__(self, '_data')
return internal.get(name, "default")
Real World Example: Lazy Loading
Here's a pattern PythonSkillset uses for lazy-loaded database models:
class LazyModel:
def __init__(self, model_name):
self._model_name = model_name
self._loaded = False
self._data = {}
def __getattr__(self, name):
if not self._loaded:
# Trigger lazy loading
print(f"Loading {self._model_name} data...")
self._load_data()
self._loaded = True
# Now try to get the attribute
if name in self._data:
return self._data[name]
raise AttributeError(f"{self._model_name} has no attribute '{name}'")
def __getattribute__(self, name):
# Log access for debugging (never intentionally)
print(f"Accessing {name} on {object.__getattribute__(self, '_model_name')}")
return object.__getattribute__(self, name)
def _load_data(self):
self._data = {"name": "User", "age": 30, "email": "user@example.com"}
What Actually Happens Under the Hood
When you write obj.attr, Python does this:
1. Calls type(obj).__getattribute__(obj, 'attr')
2. If that raises AttributeError, calls type(obj).__getattr__(obj, 'attr')
3. If __getattr__ also raises AttributeError, you get the normal error
This means __getattribute__ is the gatekeeper, and __getattr__ is the last resort.
My Final Advice
After years of PythonSkillset development, here's what I'd tell you:
- Start with
__getattr__- It's safer and solves 90% of use cases - Only use
__getattribute__when you absolutely need to - It's powerful but dangerous - Always use
object.__getattribute__for internal access in both methods - Test edge cases - especially what happens with
hasattr()and private attributes
The difference between these two methods might seem subtle, but understanding it will save you from some of the weirdest bugs you'll ever encounter in Python. Trust me, I've been there.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.