Factory Method¶
Intent¶
Move the decision "which concrete class do I build?" out of the code that uses the object and into a creator that makes it once. The notification service asks for "sms" and receives a Sender; adding a WhatsApp channel is a new class plus one registry entry, not another elif in every place that sends.
When to use and when not to¶
Use it when
- The concrete class depends on runtime data: a string in a request, a key in a config file, a file extension, the operating system. Without a factory, every call site repeats the same switch.
- Construction needs knowledge the caller should not carry: credentials for the SMS gateway, which subclass matches
os.name, whether to pool. - Tests need to substitute the product. The factory is the one seam to inject; a registry entry that returns a stub replaces every sender at once.
Leave it out when
- There is one concrete class.
Car(plate)needs noCarFactory; add the creator when the second class arrives. - The caller already holds the class. A class is a callable in Python, so pass it:
build(EmailSender)is a factory with no factory code. - What must stay consistent is a family of products (storage and queue from the same cloud) rather than one product; that is Abstract Factory. Many optional parts assembled in steps is Builder.
Structure¶
Two creators, one product interface: the registry factory chooses by key, the Gang of Four creator chooses by subclass; the client sees only Sender.
classDiagram
class Sender {
<<interface>>
+send(notification: Notification) Receipt
}
class EmailSender {
+sender_address: str
+send(notification: Notification) Receipt
}
class SmsSender {
+sender_number: str
+max_chars: int
+send(notification: Notification) Receipt
}
class PushSender {
+app_id: str
+send(notification: Notification) Receipt
}
class NotificationSenderFactory {
-_builders: Dict~Channel,SenderBuilder~
+with_defaults() NotificationSenderFactory
+register(channel: Channel, builder: SenderBuilder) None
+create(channel: Channel) Sender
+channels() Tuple~Channel~
}
class NotificationService {
-_factory: NotificationSenderFactory
+notify(channel: Channel, notification: Notification) Receipt
}
class Notifier {
<<abstract>>
+notify(recipient: str, subject: str, body: str) Receipt
+create_sender() Sender
}
class EmailNotifier {
+create_sender() Sender
}
Sender <|.. EmailSender
Sender <|.. SmsSender
Sender <|.. PushSender
NotificationSenderFactory ..> Sender : creates
NotificationService o-- NotificationSenderFactory
Notifier <|-- EmailNotifier
Notifier ..> Sender : create_sender
NotificationSenderFactory holds a dict from Channel to a callable that builds a sender. NotificationService depends on the factory and on Sender, never on a sender class. Notifier is the textbook form: create_sender is the factory method, and EmailNotifier overrides it.
Canonical example in Python¶
The product interface and three products first (code/patterns/factory_method.py, tested by code/patterns/tests/test_factory_method.py):
class Channel(StrEnum):
EMAIL = "email"
SMS = "sms"
PUSH = "push"
IN_APP = "in_app"
WEBHOOK = "webhook"
@dataclass(frozen=True, slots=True)
class Notification:
"""What the caller wants delivered. It knows nothing about channels."""
recipient: str
subject: str
body: str
@classmethod
def from_template(cls, recipient: str, subject: str, body: str, **fields: str) -> Notification:
"""A classmethod constructor: the same class, a different way in (``datetime.fromtimestamp``)."""
return cls(recipient, subject.format(**fields), body.format(**fields))
@dataclass(frozen=True, slots=True)
class Receipt:
"""Proof of a send. Every sender returns one, so callers stay channel-agnostic."""
channel: Channel
recipient: str
rendered: str
@runtime_checkable
class Sender(Protocol):
"""The Product interface. Concrete senders qualify by shape, not by inheritance."""
def send(self, notification: Notification) -> Receipt: ...
@dataclass(frozen=True, slots=True)
class EmailSender:
sender_address: str = "noreply@example.com"
def send(self, notification: Notification) -> Receipt:
rendered = (
f"email from {self.sender_address} to {notification.recipient}: "
f"[{notification.subject}] {notification.body}"
)
return Receipt(Channel.EMAIL, notification.recipient, rendered)
@dataclass(frozen=True, slots=True)
class SmsSender:
sender_number: str = "+10000000000"
max_chars: int = SMS_MAX_CHARS
def send(self, notification: Notification) -> Receipt:
text = f"{notification.subject}: {notification.body}"
if len(text) > self.max_chars:
text = text[: self.max_chars - 3] + "..."
rendered = f"sms from {self.sender_number} to {notification.recipient}: {text}"
return Receipt(Channel.SMS, notification.recipient, rendered)
@dataclass(frozen=True, slots=True)
class PushSender:
app_id: str = "handbook"
def send(self, notification: Notification) -> Receipt:
rendered = f"push via {self.app_id} to {notification.recipient}: {notification.subject}"
return Receipt(Channel.PUSH, notification.recipient, rendered)
The senders are frozen dataclasses that satisfy Sender by shape, and Notification.from_template is already a factory method of the smallest kind: an alternate constructor on the class itself. The creator and its client:
type SenderBuilder = Callable[[], Sender]
class NotificationSenderFactory:
"""The Creator: turns a channel into a sender without the caller naming a class.
The registry maps each ``Channel`` to a zero-argument callable. A class is such
a callable, and so is ``partial(EmailSender, sender_address=...)``, which is
how configuration reaches a product without the factory knowing about it.
The factory is an instance rather than a module global: ``main`` builds one and
a test builds its own, with stubs. Registration happens at composition time,
before any thread calls ``create``, so no lock is involved.
"""
def __init__(self, builders: Mapping[Channel, SenderBuilder] | None = None) -> None:
self._builders: dict[Channel, SenderBuilder] = dict(builders or {})
@classmethod
def with_defaults(cls) -> NotificationSenderFactory:
return cls({Channel.EMAIL: EmailSender, Channel.SMS: SmsSender, Channel.PUSH: PushSender})
def register(self, channel: Channel, builder: SenderBuilder) -> None:
if channel in self._builders:
raise ConflictError(f"a sender for {channel.value!r} is already registered")
self._builders[channel] = builder
def create(self, channel: Channel | str) -> Sender:
try:
key = Channel(channel)
except ValueError:
raise ValidationError(f"unknown channel {channel!r}") from None
try:
builder = self._builders[key]
except KeyError:
raise NotFoundError(f"no sender registered for {key.value!r}") from None
return builder()
def channels(self) -> tuple[Channel, ...]:
return tuple(self._builders)
class NotificationService:
"""The client. It depends on the factory and on ``Sender``; it never imports a sender class."""
def __init__(self, factory: NotificationSenderFactory) -> None:
self._factory = factory
def notify(self, channel: Channel | str, notification: Notification) -> Receipt:
return self._factory.create(channel).send(notification)
Four decisions to say out loud:
- The registry holds callables, not classes. A class is a zero-argument callable when its fields have defaults,
partial(EmailSender, "alerts@example.com")is one that carries configuration, andlambda: spyis one that returns a test double. The factory never learns which it got. - An instance, not a module global.
mainbuilds the factory and injects it intoNotificationService; a test builds its own with stubs. Noreset, no import-order surprises. - Two failures, two exceptions.
"fax"is not a channel at all (ValidationError, bad input);"in_app"is a real channel nobody registered (NotFoundError, bad configuration). Interviewers ask "what if the channel is misspelled?", and the answer is which line rejects it. - Normalise at the boundary.
createacceptsChannel | strand converts once, so the rest of the module only ever sees the enum.
The name comes from the Gang of Four shape, where the factory method is a hook on an abstract creator:
class Notifier(ABC):
"""The Gang of Four shape: the factory method is a hook on an abstract creator.
``notify`` is written once against ``Sender``; each subclass decides which
product ``create_sender`` returns. The choice is welded to a class hierarchy,
which is why the registry form is the one to draw first.
"""
def notify(self, recipient: str, subject: str, body: str) -> Receipt:
return self.create_sender().send(Notification(recipient, subject, body))
@abstractmethod
def create_sender(self) -> Sender: ...
class EmailNotifier(Notifier):
def __init__(self, sender_address: str) -> None:
self._sender_address = sender_address
def create_sender(self) -> Sender:
return EmailSender(self._sender_address)
class SmsNotifier(Notifier):
def create_sender(self) -> Sender:
return SmsSender()
notify is a Template Method written once against Sender; each subclass decides what create_sender returns. The choice is welded to a class hierarchy, so a new channel is a new subclass. Draw the registry first and name this form when asked where the pattern came from.
Running python -m patterns.factory_method prints:
--- one call site, three products, chosen by a key ---
email: email from noreply@example.com to ana@example.com: [Order A-42 shipped] It arrives on Tuesday.
sms: sms from +10000000000 to ana@example.com: Order A-42 shipped: It arrives on Tuesday.
push: push via handbook to ana@example.com: Order A-42 shipped
--- configuration travels in the builder; the factory never sees it ---
email from alerts@example.com to ana@example.com: [Order A-42 shipped] It arrives on Tuesday.
--- a misspelled channel and an unregistered one fail differently ---
ValidationError: unknown channel 'fax'
NotFoundError: no sender registered for 'in_app'
--- GoF shape: the factory method is a hook on the creator ---
email from billing@example.com to ana@example.com: [Invoice ready] Total 12.50 USD.
--- __init_subclass__: the class statement is the registration ---
registered: ['in_app', 'webhook']
inbox of ana@example.com: Order A-42 shipped
Pythonic variant¶
Python gives you three factories for free. A classmethod constructor is a factory method on the product: datetime.fromtimestamp, dict.fromkeys, Money.of("3.00") and Notification.from_template above all build an instance from a shape the plain constructor does not accept. A dict of callables is the registry with the class removed:
builders = {"email": EmailSender, "sms": partial(SmsSender, max_chars=70), "push": PushSender}
sender = builders[channel]() # KeyError is your NotFoundError; wrap it at the boundary
The third form makes the class statement itself the registration:
class RegisteredSender(ABC):
"""``__init_subclass__`` auto-registration: the class statement is the registration.
``class InAppSender(RegisteredSender, channel=Channel.IN_APP)`` adds the class to
the registry; there is no list to forget to update. The costs: the registry is
process-wide, so a subclass defined in a test stays registered until removed,
and import order decides what exists, so a subclass in a module nobody imports
is never registered.
"""
_registry: ClassVar[dict[Channel, type[RegisteredSender]]] = {}
channel: ClassVar[Channel]
def __init_subclass__(cls, *, channel: Channel, **kwargs: Any) -> None:
super().__init_subclass__(**kwargs)
cls.channel = channel
RegisteredSender._registry[channel] = cls
@classmethod
def for_channel(cls, channel: Channel | str) -> RegisteredSender:
try:
return cls._registry[Channel(channel)]()
except (ValueError, KeyError):
raise NotFoundError(f"no registered sender for {channel!r}") from None
@classmethod
def registered(cls) -> tuple[Channel, ...]:
return tuple(cls._registry)
@abstractmethod
def send(self, notification: Notification) -> Receipt: ...
class InAppSender(RegisteredSender, channel=Channel.IN_APP):
def send(self, notification: Notification) -> Receipt:
rendered = f"inbox of {notification.recipient}: {notification.subject}"
return Receipt(Channel.IN_APP, notification.recipient, rendered)
class WebhookSender(RegisteredSender, channel=Channel.WEBHOOK):
def send(self, notification: Notification) -> Receipt:
rendered = f"POST {notification.recipient} {{subject: {notification.subject}}}"
return Receipt(Channel.WEBHOOK, notification.recipient, rendered)
- No list to forget. Defining
InAppSenderwithchannel=Channel.IN_APPregisters it;for_channelis the factory method and the registry is the class hierarchy. - The registry is process-wide. A subclass defined in a test stays registered until the test removes it, which is the Singleton problem in a new coat.
- Import order decides what exists. A sender in a module nobody imports was never registered; frameworks solve this with an explicit
autodiscoverstep.
| Reach for | When |
|---|---|
| The constructor | One concrete class |
| A classmethod constructor | Same class, a different input shape |
| A dict of callables | A handful of products, one call site, no configuration to validate |
NotificationSenderFactory |
Several call sites, registration at start-up, meaningful errors, stubs in tests |
__init_subclass__ |
A plugin system where products are written by other teams or modules |
Real-world usage¶
pathlib.Path()returns aPosixPathor aWindowsPathdepending onos.name: a factory method hiding inside__new__, which is why you never writePosixPathyourself.- Alternate constructors are everywhere:
datetime.fromtimestamp,date.today,int.from_bytes,dict.fromkeys,Decimal.from_float. logging.getLoggeris a registry factory with a swappable product:logging.setLoggerClasschanges what it builds without changing any caller.codecs.registerandimportlib.metadata.entry_pointsare registries of factory callables; Django'sModel.objects.createand SQLAlchemy'ssessionmakerare factories configured once and called everywhere.
Related patterns and confusions¶
| Looks like Factory Method | How to tell them apart |
|---|---|
| Simple factory | A create method with a switch inside. The registry form on this page is one; the Gang of Four name strictly means the subclass hook. Say "a parameterised factory" and nobody will argue. |
| Abstract Factory | Many factory methods on one object, and the products must match: create_storage and create_queue from the same cloud. One key, one product, is this page. |
| Builder | Many optional parts assembled in steps, validated once at build; a factory makes the whole object in one call from one key. |
| Strategy | The factory chooses a strategy from a name; the strategy is the behaviour. rules_by_name on the Strategy page is the smallest factory there is. |
| Prototype | Creates by cloning a configured object instead of by calling a class; useful when set-up is expensive or the configuration is data. |
| Template Method | Notifier.notify is one; the Gang of Four Factory Method is a Template Method whose varying step happens to construct something. |
Where it appears in LLD problems¶
- Design a parking lot —
VehicleFactory.create(vehicle_type, plate)maps a string from the gate toCar,TruckorMotorcyclethrough a registry dict; adding a bus touches the registry, not the gate. - Design a vending machine (and a coffee machine) — products and beverages are built from a slot code or recipe name.
- Design a payment gateway and digital wallet — a
PaymentProcessorper payment-method string (card, UPI, net banking), each an adapter over a provider. - Design a notification service (LLD) — one sender per channel behind one interface; that problem hands the dispatcher a ready-built
{Channel: sender}map, which is this registry with the construction step removed.
Interview tips¶
Interview tip
Name the key, the registry and the error in one breath: "the gate receives 'car'; VehicleFactory.create looks it up in a dict of classes and raises ValidationError for anything unknown, so adding a bus is one entry." Then add the Python sentence: a class is a callable, so the registry can hold classes, partials or lambdas, and a test registers a stub.
Common mistake
A factory that is still an if/elif ladder, now with a class around it. create that does if channel == "email": return EmailSender() has the ceremony without the benefit: every new product edits the method. Data drives the choice (a dict, a registry, a class attribute), code stays closed. Runner-up: registering products with a decorator in modules that are never imported, and discovering it in production.
Related¶
- Abstract Factory — several products that must match
- Builder — one object from many optional parts
- Dependency Injection — how the factory reaches the service
- Design a parking lot —
VehicleFactoryand the registry idiom in a full problem - Design a notification service (LLD) — senders per channel behind one factory
- Gamma, Helm, Johnson and Vlissides, Design Patterns (1994), Factory Method
- PEP 487 — Simpler customisation of class creation
- Python documentation:
pathlib—Path