Skip to content

setattr

Fundamentals

1. Definition

The __setattr__ method is called whenever you assign to an attribute on an object:

obj.attr = value  # Triggers obj.__setattr__('attr', value)
setattr(obj, 'attr', value)  # Also triggers __setattr__

2. Method Signature

def __setattr__(self, name, value):
    # name: attribute name (string)
    # value: value being assigned
    pass

3. Universal Assignment Hook

Every attribute assignment goes through __setattr__:

self.x = 10       # Calls __setattr__('x', 10)
self.name = "hi"  # Calls __setattr__('name', "hi")
self.data = []    # Calls __setattr__('data', [])

Basic Implementation

1. Simple Override

class MyClass:
    def __setattr__(self, name, value):
        print(f"Setting {name} = {value}")
        super().__setattr__(name, value)

obj = MyClass()
obj.x = 42
# Output: Setting x = 42

obj.name = "Alice"
# Output: Setting name = Alice

2. Without super()

class MyClass:
    def __setattr__(self, name, value):
        print(f"Setting {name} = {value}")
        object.__setattr__(self, name, value)

3. Must Actually Store

# ❌ BAD - doesn't actually store anything
def __setattr__(self, name, value):
    print(f"Setting {name}")
    # Forgot to actually set it!

# ✅ GOOD
def __setattr__(self, name, value):
    print(f"Setting {name}")
    super().__setattr__(name, value)

Avoiding Recursion

1. The Problem

class Broken:
    def __setattr__(self, name, value):
        # ❌ INFINITE RECURSION!
        self.name = value  # Calls __setattr__ again!

2. Correct Approaches

Use super():

class Correct:
    def __setattr__(self, name, value):
        super().__setattr__(name, value)

Use object.__setattr__:

class Correct:
    def __setattr__(self, name, value):
        object.__setattr__(self, name, value)

Direct __dict__ access:

class Correct:
    def __setattr__(self, name, value):
        self.__dict__[name] = value

3. In __init__

Be careful in constructors:

class Example:
    def __init__(self, value):
        self.value = value  # ✅ Calls __setattr__

    def __setattr__(self, name, value):
        print(f"Setting {name}")
        super().__setattr__(name, value)

obj = Example(42)
# Output: Setting value

Practical Examples

1. Validation

class Person:
    def __setattr__(self, name, value):
        if name == 'age':
            if not isinstance(value, int):
                raise TypeError("Age must be integer")
            if value < 0 or value > 150:
                raise ValueError("Invalid age range")
        elif name == 'name':
            if not isinstance(value, str):
                raise TypeError("Name must be string")
            if not value.strip():
                raise ValueError("Name cannot be empty")

        super().__setattr__(name, value)

person = Person()
person.name = "Alice"  # ✅ OK
person.age = 30        # ✅ OK
# person.age = -5      # ❌ ValueError
# person.name = ""     # ❌ ValueError

2. Type Coercion

class TypedAttributes:
    def __setattr__(self, name, value):
        if name == 'count':
            value = int(value)  # Convert to int
        elif name == 'price':
            value = float(value)  # Convert to float
        elif name == 'name':
            value = str(value).strip()  # Convert to string

        super().__setattr__(name, value)

obj = TypedAttributes()
obj.count = "42"      # Stored as int(42)
obj.price = "19.99"   # Stored as float(19.99)
obj.name = "  hi  "   # Stored as "hi"

3. Change Tracking

class TrackedObject:
    def __init__(self):
        super().__setattr__('_changes', {})

    def __setattr__(self, name, value):
        if name != '_changes':
            # Track old value
            if hasattr(self, name):
                old_value = super().__getattribute__(name)
            else:
                old_value = None

            # Record change
            changes = super().__getattribute__('_changes')
            changes[name] = (old_value, value)

        super().__setattr__(name, value)

obj = TrackedObject()
obj.x = 10
obj.x = 20
obj.y = 30
print(obj._changes)
# {'x': (None, 10), 'x': (10, 20), 'y': (None, 30)}

Read-Only Attributes

1. Protecting Attributes

class ReadOnlyAttrs:
    def __init__(self):
        super().__setattr__('_locked', False)
        self.value = 42
        super().__setattr__('_locked', True)

    def __setattr__(self, name, value):
        if super().__getattribute__('_locked'):
            raise AttributeError("Attributes are read-only")
        super().__setattr__(name, value)

obj = ReadOnlyAttrs()
print(obj.value)      # 42
# obj.value = 100     # ❌ AttributeError
# obj.new_attr = 5    # ❌ AttributeError

2. Protecting Specific Attributes

class ProtectedID:
    def __init__(self, id_value):
        self._id = id_value
        self.name = ""

    def __setattr__(self, name, value):
        if name == '_id' and hasattr(self, '_id'):
            raise AttributeError("Cannot modify ID after initialization")
        super().__setattr__(name, value)

obj = ProtectedID(123)
obj.name = "Alice"   # ✅ OK
# obj._id = 456      # ❌ AttributeError

3. Conditional Write Protection

class Document:
    def __init__(self):
        self._finalized = False
        self.content = ""

    def finalize(self):
        self._finalized = True

    def __setattr__(self, name, value):
        if name != '_finalized':
            if hasattr(self, '_finalized') and self._finalized:
                raise AttributeError("Document is finalized")
        super().__setattr__(name, value)

doc = Document()
doc.content = "Draft"  # ✅ OK
doc.finalize()
# doc.content = "New"  # ❌ AttributeError

Logging and Debugging

1. Attribute Logger

class LoggedAttributes:
    def __setattr__(self, name, value):
        print(f"[SET] {name} = {value} (type: {type(value).__name__})")
        super().__setattr__(name, value)

obj = LoggedAttributes()
obj.x = 42
# [SET] x = 42 (type: int)
obj.name = "Alice"
# [SET] name = Alice (type: str)

2. With Timestamps

from datetime import datetime

class TimestampedAttributes:
    def __init__(self):
        super().__setattr__('_timestamps', {})

    def __setattr__(self, name, value):
        if name != '_timestamps':
            timestamps = super().__getattribute__('_timestamps')
            timestamps[name] = datetime.now()
        super().__setattr__(name, value)

obj = TimestampedAttributes()
obj.x = 10
obj.y = 20
print(obj._timestamps)
# {'x': datetime(...), 'y': datetime(...)}

3. Audit Trail

class AuditedObject:
    def __init__(self):
        super().__setattr__('_history', [])

    def __setattr__(self, name, value):
        if name != '_history':
            history = super().__getattribute__('_history')
            history.append({
                'attr': name,
                'value': value,
                'time': datetime.now()
            })
        super().__setattr__(name, value)

Interaction with Properties

1. Properties Take Priority

class Example:
    def __init__(self):
        self._value = 0

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, val):
        print("Property setter")
        self._value = val

    def __setattr__(self, name, value):
        print(f"__setattr__: {name}")
        super().__setattr__(name, value)

obj = Example()
obj.value = 42
# Output:
# __setattr__: _value
# __setattr__: value
# Property setter
# __setattr__: _value

2. Call Order

obj.value = 42
    
__setattr__('value', 42) called
    
super().__setattr__('value', 42)
    
Finds property descriptor in class
    
Calls property's __set__ method
    
Inside setter: self._value = val
    
__setattr__('_value', val) called again

3. Bypassing Properties

def __setattr__(self, name, value):
    if name == 'special':
        # Bypass property, set directly
        self.__dict__[name] = value
    else:
        super().__setattr__(name, value)

Advanced Patterns

1. Attribute Registry

class RegisteredAttributes:
    _registry = {}

    def __setattr__(self, name, value):
        # Register all attributes
        RegisteredAttributes._registry[id(self), name] = value
        super().__setattr__(name, value)

    @classmethod
    def get_all_values(cls):
        return list(cls._registry.values())

2. Proxy Pattern

class Proxy:
    def __init__(self, obj):
        object.__setattr__(self, '_obj', obj)

    def __setattr__(self, name, value):
        if name == '_obj':
            object.__setattr__(self, name, value)
        else:
            setattr(object.__getattribute__(self, '_obj'), name, value)

class Target:
    def __init__(self):
        self.value = 0

proxy = Proxy(Target())
proxy.value = 42

3. Slots Enforcement

class StrictSlots:
    __slots__ = ['x', 'y']

    def __setattr__(self, name, value):
        if name not in self.__slots__:
            raise AttributeError(f"'{name}' not in __slots__")
        super().__setattr__(name, value)

obj = StrictSlots()
obj.x = 10  # ✅ OK
# obj.z = 20  # ❌ AttributeError

Common Mistakes

1. Infinite Recursion

# ❌ BAD
def __setattr__(self, name, value):
    self.name = value  # Recursion!

# ✅ GOOD
def __setattr__(self, name, value):
    super().__setattr__(name, value)

2. Not Storing Values

# ❌ BAD - validation but no storage
def __setattr__(self, name, value):
    if isinstance(value, int):
        print("Valid")
    # Value is never stored!

# ✅ GOOD
def __setattr__(self, name, value):
    if isinstance(value, int):
        print("Valid")
    super().__setattr__(name, value)

3. Breaking __init__

# ❌ BAD
def __setattr__(self, name, value):
    if hasattr(self, 'initialized'):
        # Logic here
        pass
    # Can't set 'initialized' in __init__!

# ✅ GOOD
def __setattr__(self, name, value):
    if name == 'initialized' or not hasattr(self, 'initialized'):
        super().__setattr__(name, value)
    else:
        # Logic here
        super().__setattr__(name, value)