Every ConcreteClass() call in your business logic hard-codes a decision about which class to use. Factories move that decision somewhere else, so the code that uses an object only depends on its interface. This note covers the family: the Simple Factory idiom (one place that creates objects), a registry-based factory, and the GoF Factory Method pattern (subclasses decide what to create).
Intent#
Define an interface for creating an object, but let subclasses decide which class to instantiate. (GoF)
Analogy: a restaurant chain. Every franchise follows the same "take order → make food → serve" process, but what gets made depends on the franchise: the Italian one makes Neapolitan pizza, another makes a local variation.
The problem: creation tangled with business logic#
Here the delivery planner also decides which transport class to build:
class LogisticsApp:
def plan_delivery(self, kind: str) -> None:
if kind == "truck":
transport = Truck()
elif kind == "ship":
transport = Ship()
elif kind == "plane":
transport = Plane()
else:
raise ValueError(f"Unknown transport: {kind}")
# Every new transport type means editing this method
transport.deliver()
What goes wrong:
- Tight coupling.
LogisticsAppknows every concrete transport class. - Violates Open/Closed. Adding
Trainmeans editing working code. - Violates Single Responsibility. The method plans deliveries and decides how objects are constructed.
- Duplication. The same
if/elseblock tends to get copy-pasted (aRefundServiceneeds it too). - Hard to test. You can't easily substitute a fake transport.
Option 1: Simple Factory (an idiom, not a GoF pattern)#
Idea: "I don't want constructor calls for concrete classes scattered around. Put creation in one place." Like a restaurant with one kitchen: you say what you want, the kitchen makes it.
from abc import ABC, abstractmethod
class Pizza(ABC):
@abstractmethod
def prepare(self) -> None: ...
@abstractmethod
def bake(self) -> None: ...
@property
@abstractmethod
def name(self) -> str: ...
class CheesePizza(Pizza):
def prepare(self) -> None:
print("Adding cheese topping")
def bake(self) -> None:
print("Baking 15 minutes at 350°F")
@property
def name(self) -> str:
return "Cheese Pizza"
class VeggiePizza(Pizza):
def prepare(self) -> None:
print("Adding mushrooms, peppers, onions")
def bake(self) -> None:
print("Baking 20 minutes at 300°F")
@property
def name(self) -> str:
return "Veggie Pizza"
class SimplePizzaFactory:
"""The Simple Factory: all creation decisions live here."""
@staticmethod
def create_pizza(kind: str) -> Pizza:
match kind.lower():
case "cheese":
return CheesePizza()
case "veggie":
return VeggiePizza()
case _:
raise ValueError(f"Unknown pizza type: {kind}")
class PizzaStore:
# Client: never names a concrete class, only the Pizza interface
def order_pizza(self, kind: str) -> Pizza:
pizza = SimplePizzaFactory.create_pizza(kind)
pizza.prepare()
pizza.bake()
return pizza
print(PizzaStore().order_pizza("veggie").name) # Veggie Pizza
Use it when you have a few related types (2–5), creation is straightforward, and types rarely change.
Avoid it when the switch would grow huge, creation logic is complex, or types change often. Adding a type still means editing the factory, so it doesn't satisfy Open/Closed.
Option 2: Registry-based factory#
You can remove the if/elif chain entirely by mapping names to classes. New types register themselves, so the factory's core code never changes.
from abc import ABC, abstractmethod
from typing import Dict, Type
class PaymentMethod(ABC):
"""The product interface: everything the client is allowed to know."""
@abstractmethod
def pay(self, amount: float) -> str: ...
class CreditCardPayment(PaymentMethod):
def __init__(self, fee_percentage: float = 0.02):
self.fee_percentage = fee_percentage
def pay(self, amount: float) -> str:
total = amount * (1 + self.fee_percentage)
return f"Paid ${total:.2f} via Credit Card (with fee)"
class PayPalPayment(PaymentMethod):
def pay(self, amount: float) -> str:
return f"Paid ${amount:.2f} via PayPal"
class ApplePayPayment(PaymentMethod):
def pay(self, amount: float) -> str:
return f"Paid ${amount:.2f} via Apple Pay"
class PaymentFactory:
# Name -> class lookup replaces the if/elif chain
_registry: Dict[str, Type[PaymentMethod]] = {
"CREDIT_CARD": CreditCardPayment,
"PAYPAL": PayPalPayment,
"APPLE_PAY": ApplePayPayment,
}
@classmethod
def create(cls, payment_type: str) -> PaymentMethod:
payment_cls = cls._registry.get(payment_type.upper())
if payment_cls is None:
raise ValueError(f"Unsupported payment method: {payment_type}")
return payment_cls()
@classmethod
def register(cls, name: str, payment_cls: Type[PaymentMethod]) -> None:
"""New payment types plug in without editing this class."""
cls._registry[name.upper()] = payment_cls
class CheckoutService:
def process_order(self, payment_type: str, amount: float) -> str:
# 1. Ask for the object
payment = PaymentFactory.create(payment_type)
# 2. Use it via the interface
return payment.pay(amount)
print(CheckoutService().process_order("paypal", 20.0))
# Paid $20.00 via PayPal
CheckoutService has no idea what CreditCardPayment is or how it's built. To add crypto tomorrow, you write CryptoPayment and call PaymentFactory.register("CRYPTO", CryptoPayment). Neither CheckoutService nor the factory's logic changes.
Option 3: Factory Method (GoF)#
The "method" in Factory Method is an abstract creation method that subclasses override. The base class contains business logic that needs a product; each subclass decides which product.
Structure#
| Participant | Role |
|---|---|
| Product | Interface for the objects being created |
| Concrete products | Implementations (EmailNotification, SMSNotification) |
| Creator (abstract) | Declares create_product() and has business logic that calls it |
| Concrete creators | Override create_product() to return a specific product |
In the notification example below, the pieces fit together like this:
NotificationFactory(abstract creator)create_notification(): abstract, so subclasses decidenotify(message): concrete, and callscreate_notification()- Subclasses:
EmailNotificationFactorycreates anEmailNotificationSMSNotificationFactorycreates anSMSNotification
Example#
Each concrete creator overrides one method; the business logic in notify() is written once:
from abc import ABC, abstractmethod
# Product interface
class Notification(ABC):
@abstractmethod
def send(self, message: str) -> None: ...
# Concrete products (these are NOT factories)
class EmailNotification(Notification):
def send(self, message: str) -> None:
print(f"Email: {message}")
class SMSNotification(Notification):
def send(self, message: str) -> None:
print(f"SMS: {message}")
# Creator: declares the factory method, and uses it
class NotificationFactory(ABC):
@abstractmethod
def create_notification(self) -> Notification: # the factory method
...
# Business logic is written once, against the interface
def notify(self, message: str) -> None:
notification = self.create_notification()
notification.send(message)
# Concrete creators decide WHAT to create
class EmailNotificationFactory(NotificationFactory):
def create_notification(self) -> Notification:
return EmailNotification()
class SMSNotificationFactory(NotificationFactory):
def create_notification(self) -> Notification:
return SMSNotification()
if __name__ == "__main__":
factory: NotificationFactory = EmailNotificationFactory()
factory.notify("Hello") # Email: Hello
factory = SMSNotificationFactory()
factory.notify("Hello") # SMS: Hello
Naming tip: anything with "Factory" in the name creates objects; anything without it is the product. EmailNotificationFactory creates EmailNotification.
The pattern separates two concerns:
- What to create is decided by the concrete creator.
- How to use it is defined once in the abstract creator.
It's about deferring a decision: the base class says "I need a product" and lets subclasses choose, like a head office setting policy while regional offices pick local details.
Recipe#
- Define a product interface; concrete products implement it.
- Write an abstract creator with an abstract
create_product()returning the interface, plus concrete methods that call it. - Write concrete creators that override
create_product(). - Client:
c = ConcreteCreator(), thenc.business_logic().
Simple Factory vs Factory Method#
The two are often confused. They differ in where the creation decision lives:
| Aspect | Simple Factory | Factory Method |
|---|---|---|
| Structure | One class, usually a static method or a function | Abstract class with an abstract method |
| Decision logic | Centralized (switch or registry) | Spread across subclasses |
| Adding a type | Edit the factory (or register) | Add a new subclass |
| Open/Closed | Violated (unless registry-based) | Followed |
| Best for | A few stable types | Frameworks and plugins where users add types |
| Status | Programming idiom | GoF pattern |
In one sentence each:
- Simple Factory: "I have 3–4 pizza types, they rarely change, I just want creation in one place."
- Factory Method: "I'm building a framework where users add their own transport types without touching my code."
- Abstract Factory: "I need a whole family of related objects that must match."
Real-world examples#
The standard library and DI frameworks are full of factories:
iter(obj)callsobj.__iter__(): each collection returns its own iterator type, a factory method in action.- Alternative constructors such as
datetime.fromtimestamp()andint.from_bytes(): class-method factories. logging.getLogger(name): creates a logger the first time, then returns the same one.sqlite3.connect(): returns a database-specific connection object.- Dependency-injection containers are sophisticated factories that replace much hand-written factory code in large apps.
When to use it#
A factory is worth it when:
- You don't know the exact type until runtime.
- You're building a library or framework and want users to plug in their own types.
- You want creation logic in one place, or need to mock creation in tests.
- You're following dependency inversion: depend on abstractions, not concrete classes.
When not to use it#
Skip it when:
- Creation is simple and never varies. Just call the constructor.
- You'd be adding classes with no real flexibility gained (over-engineering).
- The coupling to a concrete class is actually fine.
In functional styles, a plain function that returns an object often does the job without a class hierarchy.
Pitfalls#
Watch for these mistakes:
- Confusing the two. Simple Factory = one class with a creation method. Factory Method = abstract method overridden by subclasses.
- Factories for everything. Only add one where there is real variation.
- Returning concrete types. The factory should return the interface or abstract type.
- Parameter soup. If deciding what to create needs many parameters, rethink the design.
- Parallel hierarchies. Every new product can need a matching creator subclass, which adds classes.
Key takeaways#
- Factories separate creating an object from using it; the client depends only on the interface.
- A Simple Factory centralizes creation; a registry lets you add types without editing it.
- Factory Method uses an abstract creation method that subclasses override, which satisfies Open/Closed.
- Factory Method creates one product; Abstract Factory creates a family.
- Don't add a factory unless creation actually varies.