Attribute access in Python is a fundamental concept controlled by a set of special methods known as the attribute access dunder methods. These methods—__getattr__, __getattribute__, __setattr__, and __delattr__—form the core of the Python Data Model’s mechanism for customizing how objects interact with the dot (.) operator. Understanding their interplay, invocation order, and potential pitfalls is crucial for advanced Python programming, enabling the creation of dynamic, flexible, and robust classes.

The Default Behavior and setattr

By default, when you assign an attribute like obj.x = 10, Python stores the value 10 in the object’s instance dictionary, obj.__dict__['x']. This default behavior is implemented by the object.__setattr__ method. When you override this method, you intercept all attribute assignment attempts. Therefore, it is absolutely critical to avoid using the dot notation for assignment within your custom __setattr__ method, as it would cause infinite recursion. Instead, you must use the super() method or directly manipulate the object’s __dict__.

class ValidatedSetattr:
    def __setattr__(self, name, value):
        # Example: Validate that 'age' is a positive integer
        if name == 'age':
            if not isinstance(value, int) or value < 0:
                raise ValueError("Age must be a positive integer")
        
        # Use the parent class's __setattr__ to avoid recursion
        super().__setattr__(name, value)
        # Alternatively: self.__dict__[name] = value

obj = ValidatedSetattr()
obj.name = "Alice"  # Works fine
obj.age = 30        # Works fine
# obj.age = -5      # Raises ValueError

The Two-Tiered Get Mechanism: getattribute and getattr

Python uses a two-method system for attribute retrieval. The first, __getattribute__, is called for every single attribute access attempt, regardless of whether the attribute exists. Its job is to try to find the attribute through the normal channels (e.g., the instance dictionary, the class hierarchy). If __getattribute__ successfully finds the attribute, it returns it. If it fails to find it, it raises an AttributeError.

This is where __getattr__ comes in. It acts as a fallback mechanism. It is only invoked if __getattribute__ fails to locate the attribute. Its purpose is to handle attempts to access non-existent attributes, allowing for dynamic attribute creation, lazy loading, or graceful degradation.

class DynamicAttributes:
    def __init__(self):
        self.existing_attr = "I exist"

    def __getattribute__(self, name):
        # This is called for EVERY attribute access
        print(f"__getattribute__ called for '{name}'")
        # You MUST call the super method to avoid recursion and get normal lookup
        return super().__getattribute__(name)

    def __getattr__(self, name):
        # This is only called if __getattribute__ fails (raises AttributeError)
        print(f"__getattr__ called for missing attribute '{name}'")
        if name.startswith("dynamic_"):
            value = f"Dynamically created value for {name}"
            setattr(self, name, value)  # Create it for next time
            return value
        raise AttributeError(f"{self.__class__.__name__} has no attribute '{name}'")

obj = DynamicAttributes()
print(obj.existing_attr)  # Accessed via __getattribute__ only
# Output:
# __getattribute__ called for 'existing_attr'
# I exist

print(obj.dynamic_thing)   # Missing, so both methods are called
# Output:
# __getattribute__ called for 'dynamic_thing'
# __getattr__ called for missing attribute 'dynamic_thing'
# Dynamically created value for dynamic_thing

print(obj.dynamic_thing)   # Now it exists, so only __getattribute__ is called
# Output:
# __getattribute__ called for 'dynamic_thing'
# Dynamically created value for dynamic_thing

The Danger of Recursion in getattribute

Overriding __getattribute__ is extremely powerful but also dangerous. Any attribute access inside the method itself will trigger another call to __getattribute__, leading to infinite recursion and a quick RecursionError. The correct way to implement it is to always use the base class implementation, typically via super() or directly calling object.__getattribute__(self, name).

class SafeGetattribute:
    def __init__(self):
        self.data = "important info"

    def __getattribute__(self, name):
        # This will cause infinite recursion!
        # return self.__dict__[name]  # Accessing self.__dict__ calls __getattribute__ again!

        # This is the safe way: use super() or object.__getattribute__
        try:
            existing_val = super().__getattribute__(name)
            print(f"Found {name}: {existing_val}")
            return existing_val
        except AttributeError:
            raise AttributeError(f"Attribute {name} not found") from None

obj = SafeGetattribute()
print(obj.data)  # Works correctly

Controlling Deletion with delattr

The __delattr__ method is called when attempting to delete an attribute using del obj.attr. Like __setattr__, it intercepts all deletion attempts, so you must use super() or manipulate __dict__ inside it to avoid recursion.

class ImmutableAttributes:
    def __init__(self, fixed_value):
        super().__setattr__('fixed_value', fixed_value)  # Bypass our own __setattr__

    def __setattr__(self, name, value):
        raise AttributeError(f"{self.__class__.__name__} instances are immutable")

    def __delattr__(self, name):
        raise AttributeError(f"{self.__class__.__name__} instances are immutable")

obj = ImmutableAttributes(42)
print(obj.fixed_value)  # 42
# obj.new_value = 10    # Raises AttributeError
# del obj.fixed_value   # Raises AttributeError

Best Practices and Common Pitfalls

  1. Prefer __getattr__ over __getattribute__: Only use __getattribute__ if you need to intercept every single attribute access. For most use cases like lazy loading or providing defaults for missing attributes, __getattr__ is simpler and safer.

  2. Always use super(): Within these methods, especially __setattr__, __delattr__, and __getattribute__, using super().__setattr__(name, value) is the standard and safest way to perform the actual attribute operation and avoid recursion.

  3. Infinite Recursion: This is the most common pitfall. Any statement inside __setattr__, __getattribute__, or __delattr__ that uses the dot operator on self will re-trigger the same method. Use super() or object.__getattribute__(self, ...) to break the cycle.

  4. Order of Operations: Remember the precise order: __setattr__ and __delattr__ are always called. For gets, __getattribute__ is always called first; __getattr__ is only called as a fallback if an AttributeError is raised from __getattribute__.

  5. Performance: Intercepting every attribute access with __getattribute__ adds overhead. Avoid it in performance-critical sections of your code unless absolutely necessary.