Creational Patterns
Creational patterns control how objects are created. They remove direct coupling between code and specific classes.
Singleton
Ensures only one instance of a class exists. Useful for shared resources: config, connection pool, logger.
class Config:
_instance: "Config | None" = None
def __new__(cls) -> "Config":
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
cfg_a = Config()
cfg_b = Config()
assert cfg_a is cfg_b # True
Python note: a module itself is a singleton — imported once, cached. Prefer a module-level instance over a Singleton class when possible.
Risks: global state hides dependencies. Hard to test in isolation (tests share state). Use with discipline — pass the singleton through DI rather than accessing it globally.
Factory Method
Define an interface for creating an object, but let subclasses decide which class to instantiate.
from abc import ABC, abstractmethod
class Notification(ABC):
@abstractmethod
def send(self, message: str) -> None: ...
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}")
def get_notifier(channel: str) -> Notification:
match channel:
case "email": return EmailNotification()
case "sms": return SMSNotification()
case _: raise ValueError(f"Unknown channel: {channel}")
Use when: the exact type to create depends on runtime configuration or input data.
Abstract Factory
Create families of related objects without specifying their concrete classes.
from abc import ABC, abstractmethod
class Button(ABC):
@abstractmethod
def render(self) -> str: ...
class Checkbox(ABC):
@abstractmethod
def render(self) -> str: ...
class UIFactory(ABC):
@abstractmethod
def create_button(self) -> Button: ...
@abstractmethod
def create_checkbox(self) -> Checkbox: ...
class DarkButton(Button):
def render(self) -> str: return "<button class=dark>"
class DarkCheckbox(Checkbox):
def render(self) -> str: return "<input class=dark type=checkbox>"
class DarkThemeFactory(UIFactory):
def create_button(self) -> Button: return DarkButton()
def create_checkbox(self) -> Checkbox: return DarkCheckbox()
Use when: you need to ensure that products from the same family work together (e.g. all UI widgets use the same theme).
Builder
Build complex objects step by step. Separate construction from representation.
from dataclasses import dataclass, field
@dataclass
class QueryBuilder:
_table: str = ""
_conditions: list[str] = field(default_factory=list)
_limit: int | None = None
def from_table(self, name: str) -> "QueryBuilder":
self._table = name
return self
def where(self, condition: str) -> "QueryBuilder":
self._conditions.append(condition)
return self
def limit(self, n: int) -> "QueryBuilder":
self._limit = n
return self
def build(self) -> str:
sql = f"SELECT * FROM {self._table}"
if self._conditions:
sql += " WHERE " + " AND ".join(self._conditions)
if self._limit is not None:
sql += f" LIMIT {self._limit}"
return sql
query = (
QueryBuilder()
.from_table("orders")
.where("status = 'active'")
.limit(50)
.build()
)
Use when: an object has many optional parts, or when you need different representations of the same data.
Prototype
Create new objects by copying an existing instance (clone).
import copy
from dataclasses import dataclass
@dataclass
class ReportTemplate:
title: str
sections: list[str]
metadata: dict
base = ReportTemplate("Q4 Report", ["Summary", "Details"], {"author": "Alice"})
q4_version = copy.deepcopy(base)
q4_version.metadata["version"] = "v2"
Use when: creating a new instance is expensive (DB load, complex initialization), and you can start from a known good state.
Use Cases and Risks
| Pattern | When to use | Risk |
|---|---|---|
| Singleton | Shared resource, single config | Global state, test pollution |
| Factory Method | Runtime type decision | Can become complex if too many types |
| Abstract Factory | Related object families | More classes, more indirection |
| Builder | Complex object with many options | Verbose if object is simple |
| Prototype | Expensive creation, clone variants | Deep copy pitfalls, hidden state |