The factory pattern solves a specific problem: your code needs to create an object, but which class to instantiate depends on runtime information. Without a factory, this logic scatters across the codebase in if-elif chains that must be updated every time a new type is added.
A factory centralizes that decision in one place. The rest of the code calls the factory and receives the right object without knowing or caring which class was chosen. This article covers the three most useful factory variants in Python, building on the overview in common design patterns in Python.
The simple factory function
A function that takes a parameter and returns the right object. This is the most common form of factory in Python and often the only one you need:
def create_parser(filepath):
if filepath.endswith(".json"):
return JSONParser()
if filepath.endswith(".xml"):
return XMLParser()
if filepath.endswith(".csv"):
return CSVParser()
raise ValueError(f"No parser for: {filepath}")The caller writes parser = create_parser(report_path) and receives a parser with a .parse() method. Adding a new format means adding one class and one branch in this function. The calling code never changes.
When the number of types grows beyond four or five, replace the if-elif chain with a dictionary. The mapping is cleaner and adding a new type is a single line:
_parsers = {
".json": JSONParser,
".xml": XMLParser,
".csv": CSVParser,
}
def create_parser(filepath):
ext = os.path.splitext(filepath)[1]
parser_class = _parsers.get(ext)
if parser_class is None:
raise ValueError(f"No parser for: {filepath}")
return parser_class()The registry-based factory with decorators
When classes are spread across multiple files, a central dictionary becomes hard to maintain. A registry-based factory lets each class register itself using a decorator. The decorator stores the class in a dictionary keyed by file extension. Here is the registry and its decorator:
_parser_registry = {}
def register_parser(extension):
def decorator(cls):
_parser_registry[extension] = cls
return cls
return decorator
def create_parser(filepath):
ext = os.path.splitext(filepath)[1]
parser_class = _parser_registry.get(ext)
if parser_class is None:
raise ValueError(f"No parser registered for: {filepath}")
return parser_class()Each parser class decorates itself with its file extension. The registry fills automatically as modules are imported, so a new parser only needs to exist somewhere the application imports it from:
@register_parser(".json")
class JSONParser:
def parse(self, content):
return json.loads(content)
@register_parser(".xml")
class XMLParser:
def parse(self, content):
return xmltodict.parse(content)Adding CSV support means creating a new class with @register_parser(".csv") and importing it. The factory and all existing parsers stay untouched. This is the open-closed principle from SOLID principles in action.
The abstract factory
An abstract factory is a class that creates families of related objects. Use it when your system needs to work with multiple implementations of a set of services, such as different database backends or cloud providers.
A cloud storage factory creates the right client and uploader for AWS or Azure based on configuration:
from abc import ABC, abstractmethod
class CloudFactory(ABC):
@abstractmethod
def create_storage_client(self):
pass
@abstractmethod
def create_file_uploader(self):
pass
class AWSFactory(CloudFactory):
def create_storage_client(self):
return S3Client()
def create_file_uploader(self):
return S3Uploader()
class AzureFactory(CloudFactory):
def create_storage_client(self):
return BlobClient()
def create_file_uploader(self):
return BlobUploader()The application code receives a CloudFactory and calls its methods without knowing whether AWS or Azure is underneath. Swapping providers means passing a different factory at startup.
A dictionary lookup picks the right factory from configuration. The rest of the application never references AWS or Azure directly:
_factories = {
"aws": AWSFactory,
"azure": AzureFactory,
}
def get_cloud_factory(provider_name):
factory_class = _factories.get(provider_name)
if factory_class is None:
raise ValueError(f"Unknown provider: {provider_name}")
return factory_class()When not to use a factory
A factory adds indirection. If you always create the same class with the same arguments, a direct call is clearer. Do not wrap User(name="Ada") in a factory just because a pattern exists.
Use a factory when the class selection depends on runtime data that varies across calls. Use a registry when many classes need to be discoverable from different modules. Use an abstract factory when an entire family of related objects must be swappable as a unit.
Rune AI
Key Insights
- A factory encapsulates object creation so callers do not need to know which class to instantiate.
- Use a simple function when the mapping from input to class is a few branches.
- Use a registry dict or decorator when many classes need to be discoverable.
- Factories keep creation logic in one place, making it easy to add new types.
Frequently Asked Questions
When should I use a factory instead of calling a class directly?
What is the difference between a factory function and a factory class?
Conclusion
The factory pattern is one of the most practical design patterns for Python. It centralizes object creation, keeps if-elif chains out of your business logic, and makes adding new types a matter of registering a new class. Use a function for simple cases and a registry for extensible systems.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
Organize and Run Python Tests
Learn how to structure test directories, run tests with unittest and pytest, configure test discovery, and integrate tests into your workflow.