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 = value

into 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) again

To safely store values, you must bypass the overridden __setattr__ method using one of two standard approaches:

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] = value

Common 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.