Card 06/ 09

ComparisonDifficulty: Intermediate2 min

Factory Method Subclasses vs Passing a Class in Python

In Python a class is already a callable that makes objects, so a parameter can replace the subclass.

In Python, a class is an object you can pass as an argument, and calling it creates an instance. That means a creator can receive the class it should create, or any function that returns a product, instead of having a subclass override a method. This card sets the two forms side by side and says what the shorter one loses.

The listing is the notification sender again, with the subclasses removed. OrderNotifier takes make_sender, anything that can be called with no arguments to produce a sender.

py
from collections.abc import Callable
from typing import Protocol


class Sender(Protocol):
    def send(self, text: str) -> str: ...


class EmailSender:
    def send(self, text: str) -> str:
        return f"email: {text}"


class SmsSender:
    def __init__(self, number: str = "+44 7700 900123") -> None:
        self.number = number

    def send(self, text: str) -> str:
        return f"SMS to {self.number}: {text}"


class OrderNotifier:
    def __init__(self, make_sender: Callable[[], Sender]) -> None:
        self.make_sender = make_sender

    def notify_shipped(self, order_id: str) -> None:
        sender = self.make_sender()
        print(sender.send(f"Order {order_id} has shipped."))


OrderNotifier(EmailSender).notify_shipped("A-1042")
OrderNotifier(SmsSender).notify_shipped("A-1042")
OrderNotifier(lambda: SmsSender("+44 7700 900456")).notify_shipped("A-1043")
OutputRan with python:3.12-slim (Python 3.12.13)
email: Order A-1042 has shipped.
SMS to +44 7700 900123: Order A-1042 has shipped.
SMS to +44 7700 900456: Order A-1043 has shipped.
A class, and a lambda, passed where a subclass would have chosen.

EmailSender and SmsSender are passed as they are, because calling a class is how Python creates an instance. The third call passes a lambda, a function written inline, which creates an SmsSender with a different number. No EmailOrderNotifier or SmsOrderNotifier exists, and a new channel needs only its sender class.

What each form gives you

Subclass and factory methodwhen creation is part of a family of overrides

The subclass is a named place to override, so it can also change other steps, and a type checker and a reader see a class hierarchy. Frameworks use this form when the application must extend a base class anyway.

Pass the class or a callablewhen creation is the only thing that varies

There is no subclass per product, and any callable fits, whether a class, a lambda or functools.partial. The choice is made where the object is built, outside the class hierarchy, and it can change nothing except the one creation.

The pattern's roles survive in the short form: OrderNotifier is still the creator, and make_sender is still the step that decides the class. What has gone is the parallel hierarchy of creators. Most Python code takes this form, and so do Java codebases that pass a Supplier<Sender>; the subclass form is worth it when a subclass has other reasons to exist.