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!")
Proxy vs. Related Patterns
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.