Python Proxy Objects Using Dunder Methods

Python provides native support for creating proxy objects—wrappers that intercept, modify, or delegate operations to an underlying object—by leveraging dynamic attribute-lookup special methods, commonly called dunder methods. By implementing methods such as __getattr__, __setattr__, and __delattr__, a class can seamlessly forward incoming calls and variable accesses to an encapsulated instance. This mechanism allows developers to build transparent wrappers for lazy loading, access control, logging, and remote procedure calls without altering the interface of the original object.

The Role of __getattr__ in Delegation

The primary tool for building a proxy in Python is the __getattr__ method. Python invokes __getattr__ only when an attribute is not found in the object's instance dictionary (__dict__) or its class hierarchy. This makes it an ideal fallback for forwarding requests to a wrapped instance.

class Proxy:
    def __init__(self, target):
        self._target = target

    def __getattr__(self, name):
        return getattr(self._target, name)

In this implementation:

  1. When a caller accesses an attribute existing on the proxy (like _target), Python resolves it normally.
  2. When accessing any attribute not defined on the proxy, Python routes the request to __getattr__.
  3. The getattr() built-in retrieves the attribute from self._target, effectively delegating method calls and property reads to the underlying object.

Intercepting Mutations with __setattr__ and __delattr__

While __getattr__ handles attribute retrieval, proxies often need to forward mutations and deletions as well.

Unlike __getattr__, __setattr__ and __delattr__ are called unconditionally whenever an attribute is modified or deleted. To avoid infinite recursion, operations targeting the proxy's internal state must bypass the standard setter by writing directly to self.__dict__ or using object.__setattr__.

class TransparentProxy:
    def __init__(self, target):
        object.__setattr__(self, "_target", target)

    def __getattr__(self, name):
        return getattr(self._target, name)

    def __setattr__(self, name, value):
        if name == "_target":
            object.__setattr__(self, name, value)
        else:
            setattr(self._target, name, value)

    def __delattr__(self, name):
        if name == "_target":
            object.__delattr__(self, name)
        else:
            delattr(self._target, name)

By delegating __setattr__ and __delattr__ to the underlying _target, state changes apply directly to the encapsulated object.

Universal Interception with __getattribute__

When a proxy must intercept all attribute accesses—including those that exist on the proxy itself—__getattribute__ is used. Because __getattribute__ executes for every single attribute reference, any reference to self attributes must call the base class implementation via super().__getattribute__() to avoid infinite recursion.

class LoggingProxy:
    def __init__(self, target):
        super().__setattr__("_target", target)

    def __getattribute__(self, name):
        if name == "_target":
            return super().__getattribute__(name)
        
        target = super().__getattribute__("_target")
        print(f"Accessing attribute: {name}")
        return getattr(target, name)

The Limitation with Magic Methods

Standard dunder methods representing protocols (such as __len__, __iter__, and __getitem__) bypass __getattr__ and __getattribute__ during implicit invocation. CPython optimizes operations like len(proxy) or for item in proxy: by searching the class's type slots rather than the instance dictionary.

To forward special operators completely, a proxy must either explicitly define each required magic method on its class or dynamically populate its class namespace with wrapper functions that delegate to the target instance.