The Proxy pattern — a stand-in that controls access to the real object
A Proxy implements the exact same interface as the real object it stands in for, so callers can't tell the difference — but it can add a real check, a delay, or a cache in front of every call, entirely invisibly to the code using it.
3 min read
The problem: adding a check (or a cache, or a delay) means editing every call site, or the real class
class Database:
def query(self, sql):
return run_expensive_query(sql)
db = Database()
db.query("SELECT * FROM users") # every caller talks DIRECTLY to the real, expensive objectAdding access control, caching, or lazy initialization in front of Database.query means either editing Database itself (mixing "run the query" logic with "check permissions" or "cache results" logic that has nothing to do with the database's actual job), or editing every single call site to add the check manually — both real, undesirable options.
The fix: a stand-in with the exact same interface, sitting in front of the real object
class DatabaseProxy:
def __init__(self, real_database, user):
self._real_database = real_database
self._user = user
def query(self, sql):
if not self._user.is_authorized:
raise PermissionError("Not authorized to query the database")
return self._real_database.query(sql) # only reaches the REAL object if the check passes
db = DatabaseProxy(Database(), current_user)
db.query("SELECT * FROM users") # looks IDENTICAL to calling the real Database — but the check runs firstDatabaseProxy implements the exact same .query() interface as Database, so calling code can't tell the difference — but every call passes through the proxy's own logic first, which can allow it through to the real object, block it, modify it, or handle it entirely without ever touching the real object at all. Neither Database's own code nor any calling code needs to know the proxy exists at all.
Lazy initialization: a genuinely different real use of the same shape
class LazyImageProxy:
def __init__(self, filename):
self._filename = filename
self._real_image = None # NOT loaded yet — the expensive work is deferred
def display(self):
if self._real_image is None:
self._real_image = HighResImage(self._filename) # loads ONLY when actually needed
self._real_image.display()
gallery = [LazyImageProxy(f) for f in filenames] # cheap — no images actually loaded yet
gallery[3].display() # ONLY image #3 gets loaded, at the moment it's actually displayedA different real application of the same "stand-in with the same interface" shape: LazyImageProxy defers creating the genuinely expensive HighResImage until .display() is actually called, rather than when the proxy itself is constructed — a gallery of a hundred proxies is cheap to create, and each real image only gets loaded the moment it's genuinely needed, not all upfront.
How this differs from Adapter and Decorator, since all three "wrap" something
Adapter (from its own lesson) wraps an object to translate between two different interfaces — the wrapped thing's real interface doesn't match what's exposed. Decorator wraps an object to add new behavior, layering more functionality on top of what's already there, and is meant to be stacked (multiple decorators, each adding something). Proxy is different from both: it exposes the exact same interface as the thing it wraps, adds no new capability the real object doesn't already have, and its entire job is controlling access to that real object — permitting, denying, delaying, or caching a call — rather than translating or extending it.
The concrete signal a Proxy belongs somewhere
The tell: something needs to sit in front of a real object's exact same interface, controlling when or whether calls actually reach it — an access check, lazy loading of something expensive, caching identical repeated calls, or logging every call transparently — without adding new methods the real object doesn't have, and without the calling code needing to know the difference between talking to the proxy or the real thing.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why does DatabaseProxy implement the exact same .query() interface as the real Database class?
2. What does lazy initialization via a Proxy actually achieve, as shown in the LazyImageProxy example?
3. How does Proxy differ from Decorator, since both wrap an object?