Skip to content
Low Level Design Mastery Logo
LowLevelDesign Mastery

Dynamic Attributes

Control how attributes are accessed and assigned in your [classes](/oops-refresher/00-classes-and-objects).

Python provides special methods to customize attribute access and assignment behavior. These methods allow you to implement advanced patterns like lazy loading, validation, and dynamic attribute creation.

  • __getattr__(self, name): Called when accessing a non-existent attribute
  • __setattr__(self, name, value): Called every time an attribute is assigned

These act as hooks for attribute access and assignment.

dynamic_attributes.py
class DynamicAttributes:
def __init__(self):
self.existing_attr = "I exist"
def __getattr__(self, name: str):
"""Called only for missing attributes"""
return f"{name} not found, but here's a default value!"
def __setattr__(self, name: str, value):
"""Called for every attribute assignment"""
print(f"Setting attribute {name} to {value}")
super().__setattr__(name, value) # Important: use super()
obj = DynamicAttributes()
print(obj.existing_attr) # "I exist" (doesn't trigger __getattr__)
print(obj.missing_attr) # "missing_attr not found..." (triggers __getattr__)
obj.new_attr = "New Value" # Prints: "Setting attribute new_attr to New Value"
print(obj.new_attr) # "New Value"

Load expensive data only when accessed:

lazy_loader.py
class LazyLoader:
"""Load expensive data only when accessed"""
def __init__(self):
self._data = None
self._config = None
def __getattr__(self, name: str):
if name == "data":
if self._data is None:
print("Loading data from database...")
self._data = self._load_data()
return self._data
elif name == "config":
if self._config is None:
print("Loading configuration...")
self._config = self._load_config()
return self._config
raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
def _load_data(self):
# Simulate expensive operation
return {"users": 1000, "posts": 5000}
def _load_config(self):
return {"theme": "dark", "language": "en"}
loader = LazyLoader()
print(loader.data) # Prints: "Loading data..." then returns data
print(loader.data) # Returns cached data (no loading message)
print(loader.config) # Prints: "Loading configuration..." then returns config

Validate attributes before setting:

validated_attributes.py
class ValidatedUser:
"""Validate attributes before setting"""
def __init__(self):
self._age = None
self._email = None
def __setattr__(self, name: str, value):
if name == "age":
if not isinstance(value, int) or value < 0:
raise ValueError("Age must be a non-negative integer")
elif name == "email":
if "@" not in str(value):
raise ValueError("Invalid email address")
super().__setattr__(name, value)
user = ValidatedUser()
user.age = 25 # Valid
user.email = "user@example.com" # Valid
# user.age = -5 # ValueError
# user.email = "invalid" # ValueError

Log all attribute access and changes:

attribute_logging.py
class LoggedAttributes:
"""Log all attribute access and changes"""
def __init__(self):
self._attributes = {}
self._access_log = []
def __getattr__(self, name: str):
"""Log attribute access"""
if name.startswith("_"):
raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
self._access_log.append(f"Accessed: {name}")
return self._attributes.get(name, None)
def __setattr__(self, name: str, value):
"""Log attribute changes"""
if name.startswith("_"):
super().__setattr__(name, value)
else:
self._access_log.append(f"Set: {name} = {value}")
if not hasattr(self, '_attributes'):
super().__setattr__('_attributes', {})
self._attributes[name] = value
def get_log(self):
"""Get access log"""
return self._access_log
obj = LoggedAttributes()
obj.name = "Alice"
obj.age = 25
print(obj.name) # "Alice"
print(obj.get_log())
# ['Set: name = Alice', 'Set: age = 25', 'Accessed: name']

Create a proxy that forwards attribute access:

proxy_pattern.py
class Proxy:
"""Proxy that forwards attribute access to another object"""
def __init__(self, target):
self._target = target
def __getattr__(self, name: str):
"""Forward attribute access to target"""
return getattr(self._target, name)
def __setattr__(self, name: str, value):
"""Handle our own attributes, forward others"""
if name.startswith("_"):
super().__setattr__(name, value)
else:
setattr(self._target, name, value)
class User:
def __init__(self, name: str):
self.name = name
def greet(self):
return f"Hello, I'm {self.name}"
# Usage
user = User("Alice")
proxy = Proxy(user)
print(proxy.name) # "Alice" - forwarded to user
print(proxy.greet()) # "Hello, I'm Alice" - forwarded to user
proxy.name = "Bob" # Forwarded to user
print(user.name) # "Bob"

Always use super().__setattr__() to avoid infinite recursion:

super_setattr.py
class CorrectSetattr:
def __setattr__(self, name: str, value):
# Use super() to avoid infinite recursion
super().__setattr__(name, value)
class WrongSetattr:
def __setattr__(self, name: str, value):
# WRONG! Causes infinite recursion
self.name = value # This calls __setattr__ again!
getattr_vs_getattribute.py
class Example:
def __init__(self):
self.existing = "I exist"
def __getattr__(self, name: str):
"""Only called for missing attributes"""
return f"{name} not found"
def __getattribute__(self, name: str):
"""Called for EVERY attribute access"""
print(f"Accessing: {name}")
return super().__getattribute__(name)
obj = Example()
print(obj.existing) # Prints "Accessing: existing" then "I exist"
print(obj.missing) # Prints "Accessing: missing" then "missing not found"