Intercepting Python Attributes with setattr
Python provides the __setattr__ dunder method as a
low-level mechanism for intercepting, modifying, or restricting
attribute assignment on class instances. This article examines how
Python invokes __setattr__ during assignment operations,
the execution flow behind dynamic attribute interception, common
architectural patterns such as validation and immutability, and how to
avoid the critical pitfalls of infinite recursion.
How __setattr__ Works
In Python, every standard attribute assignment using the dot notation
(object.name = value) triggers the instance's
__setattr__ method. Python translates the statement:
instance.attribute = valueinto an explicit method call:
instance.__setattr__("attribute", value)Because __setattr__ is defined on object
(the base class of all new-style classes), every Python class inherits a
default implementation that handles normal assignment by updating the
instance's namespace dictionary (__dict__). Overriding this
method allows a class to intercept every assignment attempt before any
data is written to the instance.
The Recursion Trap and Safe Assignment
The primary technical challenge when implementing
__setattr__ is avoiding infinite recursion. If you attempt
to assign an attribute using standard syntax inside
__setattr__, the method calls itself repeatedly until
Python raises a RecursionError.
# Incorrect: Triggers infinite recursion
class RecursiveExample:
def __setattr__(self, name, value):
self.name = value # Calls self.__setattr__('name', value) againTo safely store values, you must bypass the overridden
__setattr__ method using one of two standard
approaches:
1. Using
super().__setattr__ (Recommended)
Delegating assignment to the parent class—typically
object—is the cleanest and most compatible approach. It
maintains normal inheritance chains and works seamlessly with properties
and descriptors.
class SuperExample:
def __setattr__(self, name, value):
# Perform custom logic here
super().__setattr__(name, value)2. Directly Mutating
__dict__
Alternatively, you can write directly to the instance's attribute
dictionary. While common, this technique bypasses descriptors, data
properties, and instances that define __slots__ without an
instance dictionary.
class DictExample:
def __setattr__(self, name, value):
# Direct dictionary insertion avoids calling __setattr__
self.__dict__[name] = valueCommon Implementation Patterns
Attribute Validation and Type Enforcement
You can enforce strict typing or value constraints dynamically
without defining individual @property methods for every
attribute.
class StrictTypeContainer:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
def __setattr__(self, name, value):
if name.startswith("int_") and not isinstance(value, int):
raise TypeError(f"Attribute '{name}' must be an integer.")
super().__setattr__(name, value)Creating Immutable Classes
By raising an AttributeError on every assignment attempt
after initialization, __setattr__ can freeze an object's
state to ensure immutability.
class FrozenObject:
def __init__(self, **kwargs):
for key, value in kwargs.items():
super().__setattr__(key, value)
super().__setattr__("_frozen", True)
def __setattr__(self, name, value):
if getattr(self, "_frozen", False):
raise AttributeError(f"Cannot modify immutable instance attribute '{name}'.")
super().__setattr__(name, value)Dynamic Proxies and Storage Redirection
__setattr__ enables dynamic proxy objects to forward
assignments to underlying structures, such as configuration
dictionaries, remote services, or database layers.
class DynamicConfig:
def __init__(self):
super().__setattr__("_storage", {})
def __setattr__(self, name, value):
if name == "_storage":
super().__setattr__(name, value)
else:
self._storage[name.lower()] = value
def __getattr__(self, name):
return self._storage[name.lower()]__setattr__
vs. Descriptors and Properties
While properties (@property) and custom descriptors
intercept attribute assignments for specific names,
__setattr__ acts as a blanket interceptor for the entire
class.
When an attribute assignment executes, Python executes
type(instance).__setattr__ first. Python's default
object.__setattr__ looks for data descriptors (like
@property.setter) on the class before writing to
__dict__. If you override __setattr__ without
calling super().__setattr__, custom descriptors and
property setters defined on that class will not be executed unless
explicitly handled inside your custom method.