Proxy Pattern in Python: Virtual and Protection Proxies

The Proxy pattern is a structural design pattern that places an intermediary object between a client and a target object. This intermediary—the proxy—controls access to the target, adding a layer of...

Key Insights

  • The Proxy pattern provides a surrogate object that controls access to another object, enabling lazy initialization (virtual proxy) or access control (protection proxy) without modifying the original class.
  • Python’s __getattr__ method enables elegant dynamic proxying that automatically delegates attribute access, reducing boilerplate while maintaining transparency.
  • Choose virtual proxies when object creation is expensive and may not always be needed; choose protection proxies when you need to enforce access rules without coupling authorization logic to business objects.

Introduction to the Proxy Pattern

The Proxy pattern is a structural design pattern that places an intermediary object between a client and a target object. This intermediary—the proxy—controls access to the target, adding a layer of indirection that enables powerful capabilities without modifying the original object.

Think of a credit card as a real-world proxy. When you make a purchase, the card acts as a surrogate for your bank account. It doesn’t contain your money directly, but it controls access to it—verifying your identity, checking your balance, and authorizing transactions. The merchant interacts with the card (proxy), not your actual bank account (real subject).

In software, proxies serve similar purposes: deferring expensive operations until absolutely necessary, controlling access based on permissions, logging method calls, or caching results. The pattern shines when you need to add cross-cutting concerns without polluting your core business logic.

Proxy Pattern Structure and Components

The Proxy pattern involves three participants:

  • Subject: An interface or abstract class defining the common operations that both the RealSubject and Proxy must implement.
  • RealSubject: The actual object that performs the real work. This is what the client ultimately wants to interact with.
  • Proxy: The intermediary that holds a reference to the RealSubject and controls access to it.

The relationship is straightforward: both Proxy and RealSubject implement the same Subject interface, making them interchangeable from the client’s perspective.

from abc import ABC, abstractmethod


class Subject(ABC):
    """Abstract interface for both RealSubject and Proxy."""
    
    @abstractmethod
    def request(self) -> str:
        """Perform the core operation."""
        pass
    
    @abstractmethod
    def get_data(self) -> dict:
        """Retrieve data from the subject."""
        pass


class RealSubject(Subject):
    """The actual object that does the real work."""
    
    def __init__(self, name: str):
        self._name = name
        self._data = {"id": 1, "content": "sensitive information"}
    
    def request(self) -> str:
        return f"RealSubject {self._name} handling request"
    
    def get_data(self) -> dict:
        return self._data

This structure ensures that clients can work with either the proxy or the real subject through the same interface, enabling transparent substitution.

Virtual Proxy: Lazy Initialization

A virtual proxy delays the creation of an expensive object until it’s actually needed. This is particularly valuable when dealing with resource-intensive objects that may never be used, or when you want to spread initialization costs across time rather than paying them all upfront.

Common use cases include:

  • Loading high-resolution images only when displayed
  • Establishing database connections on first query
  • Initializing complex computational models on demand

Here’s a practical example with image loading:

import time
from abc import ABC, abstractmethod


class Image(ABC):
    """Subject interface for images."""
    
    @abstractmethod
    def display(self) -> None:
        pass
    
    @abstractmethod
    def get_dimensions(self) -> tuple[int, int]:
        pass


class HighResolutionImage(Image):
    """Resource-intensive image that takes time to load."""
    
    def __init__(self, filename: str):
        self._filename = filename
        self._load_image()
    
    def _load_image(self) -> None:
        """Simulate expensive image loading operation."""
        print(f"Loading high-resolution image: {self._filename}")
        time.sleep(2)  # Simulate I/O delay
        self._width = 4096
        self._height = 2160
        self._data = b"x" * (self._width * self._height * 3)  # RGB data
        print(f"Loaded {len(self._data):,} bytes")
    
    def display(self) -> None:
        print(f"Displaying {self._filename} ({self._width}x{self._height})")
    
    def get_dimensions(self) -> tuple[int, int]:
        return (self._width, self._height)


class ImageProxy(Image):
    """Virtual proxy that delays image loading until display() is called."""
    
    def __init__(self, filename: str):
        self._filename = filename
        self._real_image: HighResolutionImage | None = None
    
    def _ensure_loaded(self) -> HighResolutionImage:
        """Lazy initialization - load only when needed."""
        if self._real_image is None:
            self._real_image = HighResolutionImage(self._filename)
        return self._real_image
    
    def display(self) -> None:
        self._ensure_loaded().display()
    
    def get_dimensions(self) -> tuple[int, int]:
        return self._ensure_loaded().get_dimensions()


# Usage comparison
def demonstrate_virtual_proxy():
    print("=== Without Proxy (Eager Loading) ===")
    start = time.time()
    images = [HighResolutionImage(f"photo_{i}.jpg") for i in range(3)]
    print(f"Initialization took {time.time() - start:.2f}s\n")
    
    print("=== With Proxy (Lazy Loading) ===")
    start = time.time()
    proxied_images = [ImageProxy(f"photo_{i}.jpg") for i in range(3)]
    print(f"Initialization took {time.time() - start:.2f}s")
    
    print("\nDisplaying only the first image:")
    proxied_images[0].display()  # Only this one loads

The virtual proxy creates image objects instantly. The actual loading happens only when display() or get_dimensions() is called. If you have a gallery of 100 images but the user only views 5, you’ve saved 95 expensive load operations.

Protection Proxy: Access Control

A protection proxy controls access to an object based on permissions, roles, or other conditions. This separates authorization concerns from business logic, keeping your domain objects clean.

from abc import ABC, abstractmethod
from enum import Enum, auto
from dataclasses import dataclass
from typing import Callable


class Permission(Enum):
    READ = auto()
    WRITE = auto()
    DELETE = auto()
    ADMIN = auto()


@dataclass
class User:
    username: str
    permissions: set[Permission]


class Document(ABC):
    """Subject interface for documents."""
    
    @abstractmethod
    def read(self) -> str:
        pass
    
    @abstractmethod
    def write(self, content: str) -> None:
        pass
    
    @abstractmethod
    def delete(self) -> None:
        pass


class SensitiveDocument(Document):
    """Real document containing sensitive information."""
    
    def __init__(self, title: str, content: str):
        self._title = title
        self._content = content
    
    def read(self) -> str:
        return f"[{self._title}]\n{self._content}"
    
    def write(self, content: str) -> None:
        self._content = content
        print(f"Document '{self._title}' updated")
    
    def delete(self) -> None:
        print(f"Document '{self._title}' deleted permanently")
        self._content = ""


class AccessDeniedError(Exception):
    """Raised when a user lacks required permissions."""
    pass


class DocumentProxy(Document):
    """Protection proxy that enforces access control."""
    
    def __init__(self, document: SensitiveDocument, user: User):
        self._document = document
        self._user = user
    
    def _check_permission(self, required: Permission) -> None:
        if required not in self._user.permissions:
            raise AccessDeniedError(
                f"User '{self._user.username}' lacks {required.name} permission"
            )
    
    def read(self) -> str:
        self._check_permission(Permission.READ)
        return self._document.read()
    
    def write(self, content: str) -> None:
        self._check_permission(Permission.WRITE)
        self._document.write(content)
    
    def delete(self) -> None:
        self._check_permission(Permission.DELETE)
        self._document.delete()


# Usage
admin = User("alice", {Permission.READ, Permission.WRITE, Permission.DELETE})
viewer = User("bob", {Permission.READ})

doc = SensitiveDocument("Q4 Financials", "Revenue: $10M, Profit: $2M")

admin_proxy = DocumentProxy(doc, admin)
viewer_proxy = DocumentProxy(doc, viewer)

print(viewer_proxy.read())  # Works

try:
    viewer_proxy.write("Hacked!")  # Raises AccessDeniedError
except AccessDeniedError as e:
    print(f"Access denied: {e}")

The SensitiveDocument class knows nothing about permissions. The proxy handles all authorization, making both components easier to test and maintain independently.

Implementation Variations in Python

Python offers elegant alternatives to the traditional proxy implementation. The __getattr__ method enables dynamic proxying with minimal code:

class DynamicProxy:
    """Generic proxy using __getattr__ for automatic delegation."""
    
    def __init__(self, target: object):
        self._target = target
    
    def __getattr__(self, name: str):
        """Delegate attribute access to the target object."""
        attr = getattr(self._target, name)
        if callable(attr):
            def wrapper(*args, **kwargs):
                print(f"Calling {name} with args={args}, kwargs={kwargs}")
                result = attr(*args, **kwargs)
                print(f"Returned: {result}")
                return result
            return wrapper
        return attr


# Works with any object
proxied_list = DynamicProxy([1, 2, 3])
proxied_list.append(4)  # Logs the call
print(proxied_list._target)  # [1, 2, 3, 4]

Decorators provide another Pythonic approach for method-level proxying:

from functools import wraps
from typing import Callable, TypeVar, ParamSpec

P = ParamSpec("P")
R = TypeVar("R")


def require_permission(permission: Permission):
    """Decorator-based protection proxy for methods."""
    def decorator(func: Callable[P, R]) -> Callable[P, R]:
        @wraps(func)
        def wrapper(self, *args: P.args, **kwargs: P.kwargs) -> R:
            if not hasattr(self, "_current_user"):
                raise AccessDeniedError("No user context")
            if permission not in self._current_user.permissions:
                raise AccessDeniedError(f"Requires {permission.name}")
            return func(self, *args, **kwargs)
        return wrapper
    return decorator


class ProtectedService:
    def __init__(self, user: User):
        self._current_user = user
    
    @require_permission(Permission.READ)
    def get_data(self) -> dict:
        return {"secret": "data"}
    
    @require_permission(Permission.ADMIN)
    def reset_system(self) -> None:
        print("System reset!")

The Proxy pattern is often confused with similar structural patterns:

Decorator adds new behavior to an object. A logging decorator adds logging capabilities. A proxy controls access without necessarily adding behavior—it’s about when and whether you can access the object.

Adapter changes an interface to match what a client expects. A proxy maintains the same interface as the real subject. If you’re converting between interfaces, use Adapter.

Facade simplifies a complex subsystem by providing a unified interface. It doesn’t control access to individual objects; it provides a convenient entry point to a group of objects.

Best Practices and When to Use

Choose virtual proxies when:

  • Object creation is expensive (database connections, file I/O, network resources)
  • Objects may not be used during a session
  • You want to defer initialization to spread load

Choose protection proxies when:

  • Access control logic would clutter domain objects
  • Authorization rules change independently of business logic
  • You need audit logging of access attempts

Testing strategies:

  • Test the real subject in isolation without proxy overhead
  • Test proxy logic separately with mock subjects
  • Integration tests should verify the proxy correctly delegates to the real subject

Common pitfalls to avoid:

  • Don’t create proxies for simple objects—the indirection adds complexity without benefit
  • Avoid deep proxy chains that obscure what’s actually happening
  • Be careful with __getattr__ proxies and special methods (__len__, __iter__)—they require explicit delegation

The Proxy pattern is a powerful tool for separating concerns. Virtual proxies optimize resource usage; protection proxies enforce security boundaries. Both keep your core objects focused on their primary responsibilities while the proxy handles cross-cutting concerns at the boundary.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.