Beyond the Boilerplate: Reimagining the Factory Pattern with Modern Python

The Core Principles of Object Creation

Design patterns emerged in an era where

(OOP) reigned supreme. Traditionally, the
Factory Pattern
relies heavily on classes and inheritance to achieve its goals. At its heart, the pattern serves three vital design principles. First, it separates creation from use. By injecting objects of a specific subtype into an application, the system uses those objects without needing to know their exact implementation. This reduces coupling significantly.

Second, it adheres to the Single Responsibility Principle. Creating an object and using that object are two distinct tasks that should not live in the same function. Finally, it supports the Open-Closed Principle: the system remains open for extension (adding new types of exports) but closed for modification (not changing the existing export logic). While the 90s-style implementation uses

(ABCs), modern
Python
offers more streamlined alternatives.

Structural Typing with Protocols

Beyond the Boilerplate: Reimagining the Factory Pattern with Modern Python
The Factory Design Pattern is Obsolete in Python

One of the most powerful shifts in modern Python is the move from nominal typing to structural typing via

, introduced in Python 3.8. Traditional ABCs require explicit inheritance, creating a rigid vertical hierarchy. Protocols, however, work like interfaces in
TypeScript
. If an object has the required methods, it satisfies the type.

from typing import Protocol

class VideoExporter(Protocol):
    def prepare_export(self, video_data):
        ...
    def do_export(self, folder):
        ...

By switching to Protocol, you eliminate the need for subclasses to explicitly inherit from a base. This matches Python's "duck typing" philosophy. The code remains clean, and the type checker still catches mismatches. However, note that by abandoning inheritance, you lose the ability to provide default method implementations in a superclass.

Using Tuples as Lightweight Containers

Often, a "Factory" class is just a glorified container for a few related functions or classes. You can replace entire factory hierarchies with simple

. If you need to group a specific video exporter with a specific audio exporter, a tuple does this efficiently without the overhead of a class structure.

# Mapping quality keys to tuples of classes
FACTORIES = {
    "low": (H264BPVideoExporter, AACAudioExporter),
    "high": (H264HiVideoExporter, AACAudioExporter),
    "master": (LosslessVideoExporter, WavAudioExporter)
}

You can then destructure these tuples at the point of use. This approach is incredibly fast and memory-efficient. The downside? You must remember the order of elements (e.g., video first, then audio), and adding configuration data to a tuple quickly becomes messy.

The Pythonic Sweet Spot: Dataclasses and __call__

For a balance of flexibility and readability,

combined with the __call__ dunder method provide a superior alternative to traditional factory objects. This allows you to treat a class instance like a function, providing a clean API for object construction.

from dataclasses import dataclass
from typing import Type

@dataclass
class MediaExporterFactory:
    video_class: Type[VideoExporter]
    audio_class: Type[AudioExporter]

    def __call__(self) -> MediaExporter:
        return MediaExporter(self.video_class(), self.audio_class())

This structure provides named access to components, preventing the ordering mistakes common with tuples. It maintains the decoupling of the original pattern while utilizing Python's most ergonomic features. While dataclasses are slightly slower and use more memory than tuples, the gain in code clarity is usually worth the trade-off in most application-level code. Choosing between these methods depends on your performance requirements, but moving away from rigid inheritance is almost always a win for maintainability.

Beyond the Boilerplate: Reimagining the Factory Pattern with Modern Python

Fancy watching it?

Watch the full video and context

3 min read