getattr¶
Fundamentals¶
1. Definition¶
The __getattr__ method is called only when regular attribute lookup fails:
obj.missing_attr # Triggers __getattr__ if 'missing_attr' not found
2. Method Signature¶
def __getattr__(self, name):
# Called only for missing attributes
# Can return a value or raise AttributeError
pass
3. Fallback Mechanism¶
obj.attr
↓
__getattribute__('attr') called
↓
Attribute found? → Return it
↓
Not found? → AttributeError
↓
__getattr__('attr') called (if defined)
↓
Return value or raise AttributeError
Basic Implementation¶
1. Simple Example¶
class MyClass:
def __init__(self):
self.existing = "I exist"
def __getattr__(self, name):
return f"Default value for {name}"
obj = MyClass()
print(obj.existing) # "I exist" (normal lookup)
print(obj.missing) # "Default value for missing" (__getattr__)
print(obj.anything) # "Default value for anything" (__getattr__)
2. Providing Defaults¶
class Config:
def __init__(self):
self.host = "localhost"
def __getattr__(self, name):
defaults = {
'port': 8080,
'debug': False,
'timeout': 30
}
if name in defaults:
return defaults[name]
raise AttributeError(f"No attribute: {name}")
config = Config()
print(config.host) # "localhost" (exists)
print(config.port) # 8080 (from __getattr__)
print(config.debug) # False (from __getattr__)
3. Raising Errors¶
def __getattr__(self, name):
raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
Only for Missing Attributes¶
1. Comparison¶
class Example:
def __init__(self):
self.exists = "value"
def __getattr__(self, name):
print(f"__getattr__ called for: {name}")
return "default"
obj = Example()
print(obj.exists) # "value" (__getattr__ NOT called)
print(obj.missing) # "default" (__getattr__ IS called)
2. Not Called For¶
__getattr__ is not called when:
- Attribute exists in __dict__
- Attribute is a descriptor in the class
- Attribute exists in parent classes
- Attribute is found through normal lookup
3. Only Called When¶
__getattr__ is called when:
- Attribute doesn't exist anywhere
- __getattribute__ raises AttributeError
- No other lookup mechanism found the attribute
Practical Examples¶
1. Dynamic Attributes¶
class DynamicObject:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
def __getattr__(self, name):
return f"No attribute '{name}' - returning None"
obj = DynamicObject(name="Alice", age=30)
print(obj.name) # "Alice"
print(obj.age) # 30
print(obj.email) # "No attribute 'email' - returning None"
2. Lazy Loading¶
class LazyLoader:
def __init__(self):
self._cache = {}
def __getattr__(self, name):
if name.startswith('data_'):
print(f"Loading {name}...")
# Simulate expensive operation
data = f"Loaded data for {name}"
# Cache it
self._cache[name] = data
setattr(self, name, data) # Store in __dict__
return data
raise AttributeError(f"No attribute: {name}")
loader = LazyLoader()
print(loader.data_users) # Loading data_users... Loaded data for data_users
print(loader.data_users) # Loaded data for data_users (from __dict__, __getattr__ not called)
3. Attribute Delegation¶
class Wrapper:
def __init__(self, wrapped_object):
self._wrapped = wrapped_object
def __getattr__(self, name):
# Delegate to wrapped object
return getattr(self._wrapped, name)
class Target:
def __init__(self):
self.value = 42
def method(self):
return "method called"
wrapper = Wrapper(Target())
print(wrapper.value) # 42 (delegated)
print(wrapper.method()) # "method called" (delegated)
vs getattribute¶
1. Key Differences¶
| Aspect | __getattribute__ |
__getattr__ |
|---|---|---|
| When called | Every attribute access | Only missing attributes |
| Frequency | Always | Rarely (only on failure) |
| Use case | Universal interception | Graceful fallback |
| Complexity | High (easy to break) | Low (safer) |
| Performance | Slower (always runs) | Faster (rarely runs) |
2. Example¶
class Comparison:
def __init__(self):
self.exists = "value"
def __getattribute__(self, name):
print(f"__getattribute__: {name}")
return super().__getattribute__(name)
def __getattr__(self, name):
print(f"__getattr__: {name}")
return "default"
obj = Comparison()
print("=" * 40)
print(obj.exists)
# __getattribute__: exists
# value
print("=" * 40)
print(obj.missing)
# __getattribute__: missing
# __getattr__: missing
# default
3. When to Choose¶
Use __getattr__:
- Providing default values
- Delegating to another object
- Lazy loading attributes
- Backward compatibility (old attribute names)
Use __getattribute__:
- Logging all access
- Proxying everything
- Universal access control
- Complete attribute control
Advanced Patterns¶
1. Database-Backed Attributes¶
class DatabaseModel:
def __init__(self, record_id):
self.id = record_id
self._cache = {}
def __getattr__(self, name):
# Check cache first
if name in self._cache:
return self._cache[name]
# Query database
print(f"Querying database for {name}...")
value = f"DB value for {name}"
# Cache result
self._cache[name] = value
return value
model = DatabaseModel(123)
print(model.name) # Querying database for name... DB value for name
print(model.name) # DB value for name (from cache)
print(model.email) # Querying database for email... DB value for email
2. Method Generation¶
class DynamicMethods:
def __getattr__(self, name):
if name.startswith('get_'):
field = name[4:] # Remove 'get_' prefix
return lambda: f"Getting {field}"
elif name.startswith('set_'):
field = name[4:] # Remove 'set_' prefix
return lambda value: f"Setting {field} to {value}"
raise AttributeError(f"No attribute: {name}")
obj = DynamicMethods()
print(obj.get_name()) # "Getting name"
print(obj.set_age(30)) # "Setting age to 30"
3. Nested Attribute Access¶
class NestedAccess:
def __init__(self, data):
self._data = data
def __getattr__(self, name):
if '.' in name:
# Handle nested access like 'user.profile.email'
parts = name.split('.')
value = self._data
for part in parts:
value = value.get(part)
if value is None:
raise AttributeError(f"No nested attribute: {name}")
return value
if name in self._data:
return self._data[name]
raise AttributeError(f"No attribute: {name}")
data = {
'user': {
'profile': {
'email': 'alice@example.com'
}
}
}
obj = NestedAccess(data)
print(obj.user) # {'profile': {'email': 'alice@example.com'}}
Common Pitfalls¶
1. Forgetting to Raise¶
# ❌ BAD - returns None for everything missing
def __getattr__(self, name):
if name in self.valid_attrs:
return self.valid_attrs[name]
# Forgot to raise AttributeError!
# ✅ GOOD
def __getattr__(self, name):
if name in self.valid_attrs:
return self.valid_attrs[name]
raise AttributeError(f"No attribute: {name}")
2. Infinite Recursion¶
# ❌ BAD
def __getattr__(self, name):
return self.default_value # If 'default_value' missing, infinite loop!
# ✅ GOOD
def __getattr__(self, name):
return object.__getattribute__(self, 'default_value')
3. Not Storing Loaded Values¶
# ❌ BAD - reloads every time
def __getattr__(self, name):
return self._load_from_db(name) # Always hits database!
# ✅ GOOD - cache after loading
def __getattr__(self, name):
value = self._load_from_db(name)
setattr(self, name, value) # Store in __dict__
return value
Best Practices¶
1. Clear Error Messages¶
def __getattr__(self, name):
valid = ', '.join(self._valid_attributes)
raise AttributeError(
f"'{type(self).__name__}' has no attribute '{name}'. "
f"Valid attributes: {valid}"
)
2. Document Behavior¶
class DynamicAPI:
"""
Provides dynamic attribute access.
Any attribute access will query the API endpoint
with the same name. Results are cached.
Example:
api.users # Calls /api/users
api.posts # Calls /api/posts
"""
def __getattr__(self, name):
return self._fetch_endpoint(name)
3. Limit Scope¶
def __getattr__(self, name):
# Only handle specific pattern
if not name.startswith('dynamic_'):
raise AttributeError(f"No attribute: {name}")
# Handle dynamic attributes
return self._handle_dynamic(name)