If you’ve ever had a function with five if/elif branches deciding which object to create, you’ve already felt the problem the Factory pattern solves.
The concept: instead of your code deciding which class to instantiate, you hand that job to a dedicated function or class — the factory. Your calling code just says “give me a notification sender” and the factory figures out whether that means email, SMS, or push. The caller doesn’t know and doesn’t care.
That separation sounds small. In practice it’s the difference between code you can extend in five minutes and code you’re afraid to touch.
The Problem: Notification Logic Scattered Everywhere
You’re building an app that needs to notify users, do account alerts, order confirmations, password resets. Users can receive these via email, SMS, or push notification depending on their preferences.
The naive approach: make the decision inline, every single time.
# BEFORE: Hardcoded instantiation scattered everywhere
import smtplib
from twilio.rest import Client as TwilioClient
from firebase_admin import messaging
def send_order_confirmation(user, order):
if user.notification_preference == "email":
# set up SMTP, build message, send...
smtp = smtplib.SMTP("smtp.gmail.com", 587)
smtp.sendmail("noreply@myapp.com", user.email, f"Order {order.id} confirmed!")
elif user.notification_preference == "sms":
# set up Twilio, build message, send...
client = TwilioClient(TWILIO_SID, TWILIO_TOKEN)
client.messages.create(to=user.phone, from_="+1234567890", body=f"Order {order.id} confirmed!")
elif user.notification_preference == "push":
# set up Firebase, build message, send...
message = messaging.Message(notification=messaging.Notification(title="Order confirmed"), token=user.device_token)
messaging.send(message)
def send_password_reset(user):
if user.notification_preference == "email":
smtp = smtplib.SMTP("smtp.gmail.com", 587)
smtp.sendmail("noreply@myapp.com", user.email, "Reset your password here: ...")
elif user.notification_preference == "sms":
client = TwilioClient(TWILIO_SID, TWILIO_TOKEN)
client.messages.create(to=user.phone, from_="+1234567890", body="Your password reset code is: 123456")
elif user.notification_preference == "push":
message = messaging.Message(notification=messaging.Notification(title="Password reset requested"), token=user.device_token)
messaging.send(message)
This gets ugly fast. Every function that needs to send a notification repeats the same if/elif block. Add a fourth channel lets assume WhatsApp, and you’re hunting down every one of those blocks to add a new branch. Miss one and you’ve got a bug.
There’s also no clean way to test this. To unit test send_order_confirmation, you have to mock SMTP, Twilio, and Firebase all at once.
The Fix: A Factory That Hands You the Right Sender
The solution is to pull the “which class do I need?” decision into one place, and have everything else just ask for a sender.
# AFTER: Factory pattern for notification senders
from abc import ABC, abstractmethod
# --- Step 1: Define a common interface ---
class NotificationSender(ABC):
@abstractmethod
def send(self, recipient: str, message: str) -> None:
pass
# --- Step 2: Concrete implementations ---
class EmailSender(NotificationSender):
def send(self, recipient: str, message: str) -> None:
print(f"[EMAIL] Sending to {recipient}: {message}")
# real SMTP logic here
class SMSSender(NotificationSender):
def send(self, recipient: str, message: str) -> None:
print(f"[SMS] Sending to {recipient}: {message}")
# real Twilio logic here
class PushSender(NotificationSender):
def send(self, recipient: str, message: str) -> None:
print(f"[PUSH] Sending to {recipient}: {message}")
# real Firebase logic here
# --- Step 3: The factory ---
def get_sender(channel: str) -> NotificationSender:
senders = {
"email": EmailSender,
"sms": SMSSender,
"push": PushSender,
}
sender_class = senders.get(channel)
if sender_class is None:
raise ValueError(f"Unknown notification channel: '{channel}'. Choose from: {list(senders.keys())}")
return sender_class()
# --- Usage: callers never touch the concrete classes ---
def send_order_confirmation(user, order):
sender = get_sender(user.notification_preference)
sender.send(user.contact, f"Your order {order.id} has been confirmed!")
def send_password_reset(user, reset_link):
sender = get_sender(user.notification_preference)
sender.send(user.contact, f"Reset your password: {reset_link}")Now send_order_confirmation and send_password_reset are three lines each. Adding WhatsApp support means writing a WhatsAppSender class and adding one line to the senders dict in get_sender. Nothing else changes.
The ABC base class enforces the contract which means every sender must implement send(). If you forget, Python raises a TypeError at instantiation time, not silently at runtime when a user’s notification gets dropped.
Prefer Functions? Skip the ABC.
If the abstract base class feels heavy for your use case, a lighter version works fine too. Same factory idea, no inheritance required:
# Lightweight function-based factory
def email_sender(recipient: str, message: str) -> None:
print(f"[EMAIL] To {recipient}: {message}")
def sms_sender(recipient: str, message: str) -> None:
print(f"[SMS] To {recipient}: {message}")
def push_sender(recipient: str, message: str) -> None:
print(f"[PUSH] To {recipient}: {message}")
def get_sender(channel: str):
senders = {
"email": email_sender,
"sms": sms_sender,
"push": push_sender,
}
sender = senders.get(channel)
if sender is None:
raise ValueError(f"Unknown channel: '{channel}'")
return sender
# Usage — returns a callable directly
send = get_sender(user.notification_preference)
send(user.contact, "Your order has been confirmed!")
This trades the strict interface for simplicity. You lose the TypeError-on-bad-implementation safety net, but for smaller codebases or scripts that’s a fair trade.
Class-based vs function-based factory — which to use?
- Use the class + ABC approach when your senders have state (e.g., an authenticated API client), when you’re working in a team and want the interface enforced, or when implementations might grow complex.
- Use the function-based approach for stateless operations, small codebases, or when you want the least possible boilerplate.
What About Thread Safety?
Good news: the Factory pattern has no shared mutable state by default. The factory function just creates and returns a new object on each call — there’s nothing to race over.
The one exception is if your factory caches instances (turning it into a hybrid with the Singleton pattern — more on that in a moment). If you’re caching, apply the same double-checked locking approach covered in the Singleton post. For a plain factory that creates fresh objects every call, thread safety is not something you need to think about.
Factory vs. Singleton — Which One Do You Need?
These two patterns come up together often enough that it’s worth being direct about when to reach for each.
| Singleton | Factory | |
|---|---|---|
| Creates | One shared instance, ever | A new instance per call (usually) |
| Problem it solves | Expensive setup that should only happen once | Choosing the right class without hardcoding it |
| Calling code knows | Nothing about construction | Nothing about which concrete class it gets |
| Best for | DB engines, config, cache clients, loggers | Notification senders, payment processors, parsers, auth backends |
They also combine well. A common pattern: a factory that creates senders, where each sender is itself a singleton — so the Twilio client isn’t re-authenticated on every SMS, but the calling code still just asks for an “sms” sender and gets back a ready-to-use object.
# Factory + Singleton: one instance per channel, created on first use
_sender_cache = {}
def get_sender(channel: str) -> NotificationSender:
if channel not in _sender_cache:
senders = {
"email": EmailSender,
"sms": SMSSender,
"push": PushSender,
}
sender_class = senders.get(channel)
if sender_class is None:
raise ValueError(f"Unknown channel: '{channel}'")
_sender_cache[channel] = sender_class() # cached after first creation
return _sender_cache[channel]
Now get_sender("sms") creates the Twilio client once and reuses it on every subsequent call. Clean, efficient, and still completely transparent to the caller.
Other Places Factories Earn Their Keep
Payment processors Stripe, PayPal, and Braintree all have different SDKs and APIs, but your checkout logic just needs something that can charge(amount, currency). A factory returns the right processor based on config or user preference — your order flow never imports Stripe directly.
Database drivers Switching from PostgreSQL to SQLite for tests? A factory that reads DATABASE_URL and returns the right driver means your data layer doesn’t care which database is underneath. This pairs naturally with the Repository pattern.
Authentication backends JWT, session-based, OAuth — a factory hands back the right auth handler based on request type or config. Useful in apps that support multiple auth methods simultaneously.
The Takeaway
The Factory pattern is the answer to one specific question: how do I create objects without my calling code caring which concrete class it gets?
Use it when:
- You have multiple implementations of the same interface (email, SMS, push)
- Which implementation to use is determined at runtime, not hardcode time
- You want to add new implementations without touching existing calling code
Don’t use it for one-off object creation where there’s only ever going to be one implementation — that’s just indirection for its own sake. But when your app genuinely needs to swap behavior based on config, user preference, or environment, the Factory pattern makes that swap invisible to everything else.
One factory. Many possible products. The caller never knows the difference.
Pingback: The Observer Pattern in Python: Don’t Call Us, We’ll Call You
Pingback: The Abstract Factory Pattern in Python: Families of Things That Belong Together