Beyond the Boilerplate: Reimagining the Factory Pattern with Modern Python
The Core Principles of Object Creation
Design patterns emerged in an era where
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
Structural Typing with Protocols

One of the most powerful shifts in modern Python is the move from nominal typing to structural typing via
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
# 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, __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.

Fancy watching it?
Watch the full video and context