Factory Method in Python: Complete Implementation
The Factory Method pattern defines an interface for creating objects but lets subclasses decide which class to instantiate. Instead of calling a constructor directly, client code asks a factory to...
Key Insights
- Factory Method decouples object creation from usage, letting you add new product types without modifying existing client code—essential for plugin systems and extensible architectures.
- Python offers cleaner alternatives to the classic pattern: class registries with decorators and
__init_subclass__hooks eliminate boilerplate while preserving the pattern’s benefits. - Use Factory Method when you have multiple product variants or need runtime type selection; skip it for simple cases where direct instantiation works fine.
Introduction to Factory Method Pattern
The Factory Method pattern defines an interface for creating objects but lets subclasses decide which class to instantiate. Instead of calling a constructor directly, client code asks a factory to create the object, remaining blissfully ignorant of the concrete class involved.
This matters when your code needs to work with objects that share a common interface but have different implementations. Think payment processors, document exporters, or notification channels. The pattern shines when you can’t predict which concrete class you’ll need at compile time, or when you want to isolate creation logic from business logic.
Don’t reach for Factory Method when you have a single implementation that won’t change. Direct instantiation is simpler and more readable. The pattern earns its keep when you have multiple variants, need runtime flexibility, or want to make your code testable by injecting mock factories.
The Problem Factory Method Solves
Consider a notification system that sends alerts to users. Here’s the naive approach:
class NotificationService:
def send_alert(self, user_id: str, message: str) -> None:
# Hardcoded to email - what about SMS? Push notifications?
email_sender = EmailNotification(
smtp_host="mail.example.com",
smtp_port=587,
username="alerts@example.com",
password="secret"
)
email_sender.send(user_id, message)
This code has several problems. The NotificationService is tightly coupled to EmailNotification. Adding SMS support means modifying this class. Testing requires an actual SMTP server or complex mocking of the EmailNotification constructor. The configuration is buried inside business logic.
Every time you add a notification channel, you touch existing code. Every time you change email configuration, you touch business logic. This violates the Open/Closed Principle—the code isn’t open for extension without modification.
Pattern Structure and Components
Factory Method involves four participants:
Product (interface): Defines the interface for objects the factory creates. In Python, this is typically an abstract base class or Protocol.
ConcreteProduct: Implements the Product interface. You’ll have multiple concrete products—one for each variant.
Creator (interface): Declares the factory method returning a Product. May include default implementation.
ConcreteCreator: Overrides the factory method to return a specific ConcreteProduct.
Here’s the structural foundation using Python’s abc module:
from abc import ABC, abstractmethod
class Notification(ABC):
"""Product interface - all notifications must implement this."""
@abstractmethod
def send(self, recipient: str, message: str) -> bool:
"""Send notification to recipient. Returns success status."""
pass
class NotificationFactory(ABC):
"""Creator interface - declares the factory method."""
@abstractmethod
def create_notification(self) -> Notification:
"""Factory method - subclasses decide which Notification to create."""
pass
def send_alert(self, recipient: str, message: str) -> bool:
"""Template method using the factory method."""
notification = self.create_notification()
return notification.send(recipient, message)
The send_alert method demonstrates a common pattern: the Creator uses its own factory method internally. Client code works with the Creator interface, never knowing which concrete product gets instantiated.
Basic Implementation
Let’s build a document export system supporting PDF, CSV, and JSON formats. This is a realistic scenario where Factory Method proves its worth—export formats multiply as requirements evolve.
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any
import json
import csv
import io
@dataclass
class Report:
title: str
headers: list[str]
rows: list[list[Any]]
class Exporter(ABC):
"""Product interface for document exporters."""
@property
@abstractmethod
def file_extension(self) -> str:
pass
@abstractmethod
def export(self, report: Report) -> bytes:
pass
class PDFExporter(Exporter):
"""ConcreteProduct: PDF export (simplified)."""
@property
def file_extension(self) -> str:
return ".pdf"
def export(self, report: Report) -> bytes:
# In reality, you'd use reportlab or weasyprint
content = f"PDF Document: {report.title}\n"
content += " | ".join(report.headers) + "\n"
for row in report.rows:
content += " | ".join(str(cell) for cell in row) + "\n"
return content.encode("utf-8")
class CSVExporter(Exporter):
"""ConcreteProduct: CSV export."""
@property
def file_extension(self) -> str:
return ".csv"
def export(self, report: Report) -> bytes:
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(report.headers)
writer.writerows(report.rows)
return output.getvalue().encode("utf-8")
class JSONExporter(Exporter):
"""ConcreteProduct: JSON export."""
@property
def file_extension(self) -> str:
return ".json"
def export(self, report: Report) -> bytes:
data = {
"title": report.title,
"columns": report.headers,
"data": [
dict(zip(report.headers, row))
for row in report.rows
]
}
return json.dumps(data, indent=2).encode("utf-8")
class ExportFactory(ABC):
"""Creator interface."""
@abstractmethod
def create_exporter(self) -> Exporter:
pass
def export_report(self, report: Report) -> tuple[str, bytes]:
exporter = self.create_exporter()
filename = f"{report.title}{exporter.file_extension}"
return filename, exporter.export(report)
class PDFExportFactory(ExportFactory):
def create_exporter(self) -> Exporter:
return PDFExporter()
class CSVExportFactory(ExportFactory):
def create_exporter(self) -> Exporter:
return CSVExporter()
class JSONExportFactory(ExportFactory):
def create_exporter(self) -> Exporter:
return JSONExporter()
Client code receives a factory and uses it without knowing the concrete type:
def generate_monthly_report(factory: ExportFactory) -> tuple[str, bytes]:
report = Report(
title="monthly_sales",
headers=["Product", "Units", "Revenue"],
rows=[
["Widget A", 150, 4500.00],
["Widget B", 89, 2670.00],
]
)
return factory.export_report(report)
# Usage
filename, content = generate_monthly_report(JSONExportFactory())
Pythonic Variations
The classic pattern works but feels verbose for Python. Here are idiomatic alternatives.
Registry with Decorators
This approach uses a class-level registry and decorator for automatic registration:
from typing import Type
class ExporterRegistry:
_exporters: dict[str, Type[Exporter]] = {}
@classmethod
def register(cls, format_name: str):
"""Decorator to register an exporter class."""
def decorator(exporter_class: Type[Exporter]) -> Type[Exporter]:
cls._exporters[format_name] = exporter_class
return exporter_class
return decorator
@classmethod
def create(cls, format_name: str) -> Exporter:
"""Factory method - creates exporter by format name."""
if format_name not in cls._exporters:
available = ", ".join(cls._exporters.keys())
raise ValueError(
f"Unknown format '{format_name}'. Available: {available}"
)
return cls._exporters[format_name]()
@classmethod
def available_formats(cls) -> list[str]:
return list(cls._exporters.keys())
@ExporterRegistry.register("pdf")
class PDFExporter(Exporter):
# ... implementation unchanged
@ExporterRegistry.register("csv")
class CSVExporter(Exporter):
# ... implementation unchanged
@ExporterRegistry.register("json")
class JSONExporter(Exporter):
# ... implementation unchanged
# Usage - no factory subclasses needed
exporter = ExporterRegistry.create("json")
Using __init_subclass__
Python 3.6+ offers automatic subclass registration:
class AutoRegisterExporter(Exporter):
_registry: dict[str, Type["AutoRegisterExporter"]] = {}
format_name: str # Subclasses must define this
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if hasattr(cls, "format_name"):
AutoRegisterExporter._registry[cls.format_name] = cls
@classmethod
def create(cls, format_name: str) -> "AutoRegisterExporter":
return cls._registry[format_name]()
class JSONExporter(AutoRegisterExporter):
format_name = "json"
# ... rest of implementation
Subclasses register themselves automatically upon definition—no decorators needed.
Real-World Application: Plugin Architecture
Payment processing is a perfect Factory Method use case. You need multiple gateways, runtime selection, and the ability to add new processors without modifying core code.
from abc import ABC, abstractmethod
from dataclasses import dataclass
from decimal import Decimal
from typing import Type
import os
@dataclass
class PaymentResult:
success: bool
transaction_id: str | None
error_message: str | None = None
class PaymentProcessor(ABC):
"""Product interface for payment gateways."""
@abstractmethod
def charge(
self,
amount: Decimal,
currency: str,
token: str
) -> PaymentResult:
pass
@abstractmethod
def refund(self, transaction_id: str, amount: Decimal) -> PaymentResult:
pass
class StripeProcessor(PaymentProcessor):
def __init__(self, api_key: str):
self.api_key = api_key
def charge(
self, amount: Decimal, currency: str, token: str
) -> PaymentResult:
# Stripe API integration
return PaymentResult(
success=True,
transaction_id=f"stripe_{token[:8]}"
)
def refund(self, transaction_id: str, amount: Decimal) -> PaymentResult:
return PaymentResult(success=True, transaction_id=transaction_id)
class PayPalProcessor(PaymentProcessor):
def __init__(self, client_id: str, client_secret: str):
self.client_id = client_id
self.client_secret = client_secret
def charge(
self, amount: Decimal, currency: str, token: str
) -> PaymentResult:
return PaymentResult(
success=True,
transaction_id=f"paypal_{token[:8]}"
)
def refund(self, transaction_id: str, amount: Decimal) -> PaymentResult:
return PaymentResult(success=True, transaction_id=transaction_id)
class PaymentProcessorFactory:
"""Registry-based factory with configuration injection."""
_processors: dict[str, tuple[Type[PaymentProcessor], list[str]]] = {
"stripe": (StripeProcessor, ["STRIPE_API_KEY"]),
"paypal": (PayPalProcessor, ["PAYPAL_CLIENT_ID", "PAYPAL_SECRET"]),
}
@classmethod
def create(cls, processor_name: str) -> PaymentProcessor:
if processor_name not in cls._processors:
raise ValueError(f"Unknown processor: {processor_name}")
processor_class, env_vars = cls._processors[processor_name]
config = [os.environ[var] for var in env_vars]
return processor_class(*config)
# Usage in application code
def process_order(order_id: str, amount: Decimal, gateway: str) -> PaymentResult:
processor = PaymentProcessorFactory.create(gateway)
return processor.charge(amount, "USD", f"order_{order_id}")
Testing and Trade-offs
Factory Method dramatically improves testability. Instead of mocking constructors or patching imports, inject a factory that returns mock products:
import pytest
from unittest.mock import Mock
class MockExporter(Exporter):
def __init__(self):
self.export_called = False
self.last_report = None
@property
def file_extension(self) -> str:
return ".mock"
def export(self, report: Report) -> bytes:
self.export_called = True
self.last_report = report
return b"mock content"
class MockExportFactory(ExportFactory):
def __init__(self):
self.exporter = MockExporter()
def create_exporter(self) -> Exporter:
return self.exporter
def test_report_generation():
factory = MockExportFactory()
filename, content = generate_monthly_report(factory)
assert factory.exporter.export_called
assert factory.exporter.last_report.title == "monthly_sales"
assert filename == "monthly_sales.mock"
When to avoid Factory Method: Single implementation with no anticipated variants. Simple scripts where YAGNI applies. When a plain function returning an instance suffices.
Factory Method vs. Abstract Factory: Factory Method creates one product type through inheritance. Abstract Factory creates families of related products through composition. Use Abstract Factory when products must be used together (e.g., UI components for a specific theme).
Factory Method adds indirection. That’s the point—and the cost. Use it when that indirection buys you extensibility, testability, or cleaner separation of concerns. Skip it when direct instantiation keeps your code simpler.