Loading episodes…
0:00 0:00

Visually Explained: The Proxy Design Pattern in Python

00:00
BACK TO HOME

Visually Explained: The Proxy Design Pattern in Python

10xTeam November 06, 2025 8 min read

The Proxy Design Pattern is a structural pattern that lets you provide a surrogate or placeholder for another object. A proxy controls access to the original object, allowing you to perform additional logic before or after the request gets to the original object.

Think of it like a security checkpoint. Instead of everyone having direct access to a secure facility (the Real Subject), they must first go through a guard (the Proxy). The guard can check credentials, log visitors, or deny access altogether, all without changing the facility itself.


When Do You Need a Proxy?

The Proxy pattern is incredibly versatile. It helps organize the relationships between objects and can solve several common problems in software design.

Here’s a summary of its primary use cases:

mindmap
  root((Proxy Pattern Use Cases))
    Performance
      Caching
       
     
    ::icon(fa fa-database)
        Store results of expensive operations.
      "Lazy Initialization (Virtual Proxy)"
       
     
    ::icon(fa fa-hourglass-half)
        Delay creation of heavy objects.
    Security
      "Access Control (Protection Proxy)"
       
     
    ::icon(fa fa-lock)
        Guard access to sensitive objects.
    Resilience
      "Retry Logic (Remote Proxy)"
       
     
    ::icon(fa fa-retweet)
        Handle network failures for remote services.
      Circuit Breaker
       
     
    ::icon(fa fa-bolt)
        Prevent repeated calls to a failing service.
    Monitoring
      Logging & Auditing
       
     
    ::icon(fa fa-file-alt)
        Track interactions with the real object.

In this article, we’ll build a practical example of a Caching Proxy to optimize a slow video downloader.

The Problem: Redundant, Expensive Operations

Imagine you have a RealVideoDownloader class that downloads a video from a URL. This operation is slow and resource-intensive.

# The "Real Subject"
import time

class RealVideoDownloader:
    def download_video(self, url: str) -> str:
        print(f"Connecting to {url}...")
        print("Downloading video...")
        time.sleep(5)  # Simulate a slow network operation
        video_data = f"Video data from {url}"
        print("Download complete.")
        return video_data

Now, if your application requests the same video multiple times, it will download it from scratch every single time. This is a huge waste of time and resources.

# Client code
downloader = RealVideoDownloader()

print("--- First Request ---")
downloader.download_video("https://example.com/video1.mp4")

print("\n--- Second Request (Same Video) ---")
downloader.download_video("https://example.com/video1.mp4") # Wastes another 5 seconds!

This is where a Caching Proxy comes to the rescue.

The Solution: A Caching Proxy

A Caching Proxy sits between the client and the RealVideoDownloader. When a request comes in, the proxy first checks its internal cache.

  • Cache Hit: If the video is already in the cache, the proxy returns it instantly without bothering the RealVideoDownloader.
  • Cache Miss: If the video is not in the cache, the proxy delegates the request to the RealVideoDownloader, downloads the video, saves it to the cache, and then returns it to the client.

This flow ensures that each unique video is downloaded only once.

sequenceDiagram
    participant C as Client
    participant P as CachedDownloaderProxy
    participant R as RealVideoDownloader

    C->>P: download_video("video1.mp4")
    P->>P: Check cache for "video1.mp4"
    Note right of P: Cache Miss!
    P->>R: download_video("video1.mp4")
    R-->>P: Returns video data
    P->>P: Save video data to cache
    P-->>C: Returns video data

    C->>P: download_video("video1.mp4")
    P->>P: Check cache for "video1.mp4"
    Note right of P: Cache Hit!
    P-->>C: Returns video data from cache

Implementing the Proxy Pattern in Python

Let’s refactor our code to use the Proxy pattern. A good practice is to structure our components into separate files.

caching_proxy_example/
├── main.py
└── downloaders/
    ├── __init__.py
    ├── subject.py
    ├── real_subject.py
    └── proxy.py

Step 1: Define the Common Interface (The Subject)

Both the Real Subject and the Proxy must implement the same interface so the client can treat them interchangeably. We’ll use Python’s abc module for this.

What’s an Interface? An interface is like a contract. It defines what methods a class should have, but not how they should be implemented. This allows us to swap out different implementations (like swapping the RealVideoDownloader for our CachedDownloaderProxy) without changing the client code.

# downloaders/subject.py
from abc import ABC, abstractmethod

class IVideoDownloader(ABC):
    """The Subject interface declares common operations for both RealSubject and Proxy."""
    @abstractmethod
    def download_video(self, url: str) -> str:
        pass

Step 2: Implement the Real Subject

This is our original, slow class. It now inherits from the IVideoDownloader interface.

# downloaders/real_subject.py
import time
from .subject import IVideoDownloader

class RealVideoDownloader(IVideoDownloader):
    """The RealSubject contains the core, resource-intensive business logic."""
    def download_video(self, url: str) -> str:
        print(f"Connecting to {url}...")
        print("Downloading video from the internet...")
        time.sleep(5)  # Simulate slow network
        video_data = f"VideoData({url})"
        print("Download complete.")
        return video_data

Step 3: Create the Caching Proxy

The Proxy also implements the IVideoDownloader interface. It holds a reference to the RealVideoDownloader and manages the cache.

# downloaders/proxy.py
from .subject import IVideoDownloader
from .real_subject import RealVideoDownloader

class CachedDownloaderProxy(IVideoDownloader):
    """
    The Proxy controls access to the RealSubject and implements caching.
    """
    def __init__(self) -> None:
        self._real_downloader = RealVideoDownloader()
        self._cache: dict[str, str] = {}

    def download_video(self, url: str) -> str:
        if url not in self._cache:
            print("Proxy: 'Cache miss. Delegating to real downloader.'")
            # Download the video using the real subject
            video_data = self._real_downloader.download_video(url)
            # Store the result in the cache
            self._cache[url] = video_data
            print("Proxy: 'Storing result in cache.'")
        else:
            print("Proxy: 'Cache hit! Serving video from cache.'")
        
        return self._cache[url]

[!TIP] Lazy Initialization: Notice the _real_downloader is created in the __init__ method. For very heavy objects, you could delay its creation until download_video is called for the first time. This is known as a Virtual Proxy.

Step 4: Update the Client Code

The final step is to update the client to use the proxy instead of the real subject. The beauty of this pattern is that no other client code needs to change, as they both share the same download_video method.

  # main.py
  from downloaders.real_subject import RealVideoDownloader
  from downloaders.proxy import CachedDownloaderProxy
  from downloaders.subject import IVideoDownloader

- downloader: IVideoDownloader = RealVideoDownloader()
+ downloader: IVideoDownloader = CachedDownloaderProxy()

  print("--- First Request ---")
  downloader.download_video("https://example.com/video1.mp4")

  print("\n--- Second Request (Same Video) ---")
  downloader.download_video("https://example.com/video1.mp4") # Now it's instant!

  print("\n--- Third Request (New Video) ---")
  downloader.download_video("https://example.com/video2.mp4")

When you run the updated main.py, the first request for video1.mp4 will be slow. But the second request will be served instantly from the cache!

Other Types of Proxies

While we focused on a Caching Proxy, the pattern is powerful enough for other scenarios:

  1. Protection Proxy: Checks if the client has the required permissions to execute a request. Ideal for managing user roles (admin vs. guest).
  2. Logging Proxy: Intercepts requests to log them for analytics or auditing purposes before passing them to the real subject.
  3. Remote Proxy: Provides a local representation of an object that lives in a different address space (e.g., on a remote server). It handles all the complex networking details.

Best Practices and Final Thoughts

  • Interface is Key: The power of the Proxy pattern comes from the shared interface. It allows the proxy to be a perfect stand-in for the real object.
  • Manage Cache Size: An unbounded cache can consume all available memory. In a production system, you’d use a cache with an eviction policy, like LRU (Least Recently Used).
  • Thread Safety: If your application is multi-threaded, your proxy’s cache might need locks to prevent race conditions where multiple threads try to write to the cache simultaneously.

The Proxy pattern is a fantastic tool for adding behavior to objects without modifying their source code, leading to cleaner, more maintainable, and more efficient systems.

classDiagram
    direction LR
    class IVideoDownloader {
        <<interface>>
        +download_video(url) str
    }
    class RealVideoDownloader {
        +download_video(url) str
    }
    class CachedDownloaderProxy {
        - _real_downloader: RealVideoDownloader
        - _cache: dict
        +download_video(url) str
    }
    class Client

    IVideoDownloader <|-- RealVideoDownloader
    IVideoDownloader <|-- CachedDownloaderProxy
    Client ..> IVideoDownloader : uses
    CachedDownloaderProxy ..> RealVideoDownloader : delegates to

Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?