__getattr__ and __setattr__: Dynamic Attribute Access in Python
Most of the time, Python attributes are straightforward. You set obj.x = 5, and later obj.x gives you 5. But what if you want attributes that don't actually exist — attributes that are computed on the fly, or that trigger side effects when accessed?
Python gives you full control over attribute access through special methods: __getattr__, __getattribute__, __setattr__, and __delattr__. These let you intercept every read, write, and delete operation on any attribute.
Think of these methods as a security desk at a building entrance. Every time someone tries to enter (read), drop something off (write), or remove something (delete), the desk can check, log, redirect, or deny the request.
What Is __getattr__ and When Does It Run?
__getattr__ is Python's fallback for attribute access. It's only called when the normal lookup fails — when the attribute doesn't exist in the instance dict, the class, or any parent class.
This is the key thing to remember: __getattr__ is a fallback. It only triggers for missing attributes. Attributes that exist in __dict__ or on the class are found through normal lookup and never hit __getattr__.
What Is the Difference Between __getattr__ and __getattribute__?
This is one of the most confusing distinctions in Python:
__getattr__ — called only when the attribute is not found through normal means__getattribute__ — called on every single attribute access, even for attributes that exist__getattribute__ is the nuclear option. It intercepts everything.
Here's how the full lookup chain works:
__getattribute__ is called (always)instance.__dict____getattr__ is called as a last resortHow Do You Intercept Setting and Deleting Attributes?
__setattr__ is called every time an attribute is set — including inside __init__. This means you need to be careful to avoid infinite recursion:
The pattern is the same as __getattribute__: inside __setattr__, you can't write self.x = value normally (that would call __setattr__ again). Use super().__setattr__(name, value) to actually store the attribute.
class Bad:
def __setattr__(self, name, value):
self.log = [] # Recursion!
self.__dict__[name] = valueclass Good:
def __setattr__(self, name, value):
# Option 1: super()
super().__setattr__(name, value)
# Option 2: direct __dict__
# self.__dict__[name] = valueWhat Are Real-World Uses for Dynamic Attributes?
Dynamic attribute access enables powerful patterns that would be verbose or impossible otherwise. Here are some of the most useful ones.
Notice the use of super().__setattr__('_data', data) in __init__. We can't use self._data = data because that would trigger our custom __setattr__, which tries to access self._data — but it doesn't exist yet!
What Is Python's Complete Attribute Lookup Chain?
Here's the complete picture of what happens when you access obj.x:
type(obj).__getattribute__(obj, 'x') is calledx on type(obj) and its MROobj.__dict__['x'] (the instance dictionary)x on type(obj) and its MROobj.__getattr__('x') (if defined)AttributeErrorAnd for setting obj.x = value:
type(obj).__setattr__(obj, 'x', value) is calledx on type(obj) — if found, call its __set__value in obj.__dict__['x']Practice Exercises
Create a class DefaultObj that returns a default value for any attribute that doesn't exist. The constructor takes the default value.
Regular attributes should work normally. Only missing attributes should return the default.
obj = DefaultObj('N/A')
obj.name = 'Alice'
print(obj.name) # Alice
print(obj.missing) # N/A
print(obj.whatever) # N/AWhat does the following code print?
class Magic:
x = 10
def __getattr__(self, name):
return f'fallback:{name}'
obj = Magic()
obj.y = 20
print(obj.x)
print(obj.y)
print(obj.z)Remember: __getattr__ is only called when normal lookup fails.
Create a class Frozen that allows attributes to be set in __init__ but raises AttributeError with the message 'Cannot modify frozen object' if anyone tries to set an attribute after initialization.
Use a flag _frozen to track whether initialization is complete.
obj = Frozen(name='Alice', age=30)
print(obj.name) # Alice
print(obj.age) # 30This code tries to log attribute changes, but it crashes with infinite recursion. Fix it so it prints:
Setting name = Alice
Setting age = 30
Alice is 30Create a Proxy class that wraps another object and forwards all attribute access to it. The proxy should also count how many times attributes were accessed.
The constructor takes the wrapped object. Add a method get_access_count() that returns the total number of attribute accesses on the wrapped object.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p = Person('Alice', 30)
proxy = Proxy(p)
print(proxy.name) # Alice
print(proxy.age) # 30
print(proxy.name) # Alice
print(proxy.get_access_count()) # 3