Creational Design Patterns Cheat Sheet
Singleton, Factory Method, and Builder patterns for controlling how and when objects get created, with trade-offs for each approach.
Singleton Pattern
Ensuring a class has exactly one shared instance.
class Singleton: _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instances1 = Singleton()s2 = Singleton()s1 is s2 # True, same instance# Simpler Pythonic alternative: a module is already a singleton,# or use functools.lru_cache(maxsize=None) on a factory function.
Factory Method
Delegating object instantiation to a function based on input.
from abc import ABC, abstractmethodclass Notifier(ABC): @abstractmethod def send(self, message): ...class EmailNotifier(Notifier): def send(self, message): print(f"Email: {message}")class SMSNotifier(Notifier): def send(self, message): print(f"SMS: {message}")def notifier_factory(channel: str) -> Notifier: if channel == "email": return EmailNotifier() if channel == "sms": return SMSNotifier() raise ValueError(f"unknown channel: {channel}")notifier_factory("email").send("Welcome!") # Email: Welcome!
Builder Pattern
Constructing a complex object step by step with method chaining.
class Pizza: def __init__(self): self.toppings = [] self.size = None def __repr__(self): return f"Pizza({self.size}, {self.toppings})"class PizzaBuilder: def __init__(self): self._pizza = Pizza() def size(self, size): self._pizza.size = size return self # enables method chaining def add_topping(self, topping): self._pizza.toppings.append(topping) return self def build(self): return self._pizzapizza = (PizzaBuilder() .size("large") .add_topping("cheese") .add_topping("mushroom") .build())
Creational Pattern Catalog
One-line definitions of the classic Gang of Four creational patterns.
- Singleton- Ensure a class has only one instance and provide a global point of access to it
- Factory Method- Define an interface for creating an object, but let subclasses decide which class to instantiate
- Abstract Factory- Provide an interface for creating families of related objects without specifying their concrete classes
- Builder- Separate the construction of a complex object from its representation, using step-by-step configuration
- Prototype- Create new objects by copying an existing instance instead of instantiating from scratch
Abstract Factory: Families of Related Objects
Producing consistent sets of related products without coupling client code to concrete classes.
from abc import ABC, abstractmethodclass Button(ABC): @abstractmethod def render(self): ...class Checkbox(ABC): @abstractmethod def render(self): ...class DarkButton(Button): def render(self): return "[dark button]"class DarkCheckbox(Checkbox): def render(self): return "[dark checkbox]"class LightButton(Button): def render(self): return "(light button)"class LightCheckbox(Checkbox): def render(self): return "(light checkbox)"class UIFactory(ABC): @abstractmethod def create_button(self) -> Button: ... @abstractmethod def create_checkbox(self) -> Checkbox: ...class DarkThemeFactory(UIFactory): def create_button(self): return DarkButton() def create_checkbox(self): return DarkCheckbox()class LightThemeFactory(UIFactory): def create_button(self): return LightButton() def create_checkbox(self): return LightCheckbox()def render_toolbar(factory: UIFactory): # client code never references Dark*/Light* directly print(factory.create_button().render(), factory.create_checkbox().render())render_toolbar(DarkThemeFactory()) # [dark button] [dark checkbox]
Prototype: Cloning Instead of Rebuilding
Copying a pre-configured instance is often cheaper and safer than re-running a constructor with side effects.
import copyclass Enemy: def __init__(self, kind, stats, inventory): self.kind = kind self.stats = stats # dict, mutable self.inventory = inventory # list, mutable def clone(self): return copy.deepcopy(self) # deep copy avoids shared mutable stategoblin_template = Enemy("goblin", {"hp": 20, "atk": 4}, ["dagger"])# spawn 3 independent goblins from one prototype, no re-parsing configspawned = [goblin_template.clone() for _ in range(3)]spawned[0].stats["hp"] = 5 # only affects this instanceassert spawned[1].stats["hp"] == 20assert spawned[0] is not goblin_template
Builder with a Director for Immutable Results
Separating the build recipe (Director) from the assembly steps (Builder) so the same steps can produce different presets, and freezing the product on build().
from dataclasses import dataclass, field@dataclass(frozen=True)class HttpRequest: method: str url: str headers: tuple body: str | Noneclass RequestBuilder: def __init__(self): self._method = "GET" self._url = None self._headers = {} self._body = None def method(self, m): self._method = m; return self def url(self, u): self._url = u; return self def header(self, k, v): self._headers[k] = v; return self def body(self, b): self._body = b; return self def build(self) -> HttpRequest: if not self._url: raise ValueError("url is required") # freeze into an immutable value object return HttpRequest(self._method, self._url, tuple(self._headers.items()), self._body)class RequestDirector: @staticmethod def json_post(builder: RequestBuilder, url: str, payload: str) -> HttpRequest: return (builder.method("POST").url(url) .header("Content-Type", "application/json") .body(payload).build())req = RequestDirector.json_post(RequestBuilder(), "/api/orders", '{"qty":2}')
Thread-Safe Singleton via Metaclass
A metaclass-based Singleton centralizes instance control and is safer under concurrent first access than the naive __new__ override.
import threadingclass SingletonMeta(type): _instances = {} _lock = threading.Lock() def __call__(cls, *args, **kwargs): if cls not in cls._instances: with cls._lock: # double-checked locking if cls not in cls._instances: cls._instances[cls] = super().__call__(*args, **kwargs) return cls._instances[cls]class ConnectionPool(metaclass=SingletonMeta): def __init__(self): self.connections = []p1 = ConnectionPool()p2 = ConnectionPool()assert p1 is p2# Testability escape hatch: reset the registered instance between testsdef reset_singleton(cls): SingletonMeta._instances.pop(cls, None)
Creational Pattern Pitfalls & Trade-offs
Lesser-known gotchas that show up once these patterns leave the textbook and hit a real codebase.
- Singleton + multiprocessing- A Singleton only guarantees one instance per process; each forked/spawned worker process gets its own, so it cannot coordinate shared state across processes without external storage (Redis, a DB row)
- Factory Method vs. simple dict dispatch- If subclasses add no behavior beyond construction, a plain {key: constructor} dict is simpler and just as extensible — reach for the full pattern only when creation logic is nontrivial
- Abstract Factory rigidity- Adding a new product type (e.g. Slider) forces every concrete factory and the abstract interface to change; it optimizes for adding new families, not new product kinds
- Builder without validation- A build() that skips required-field checks just moves invalid-object bugs from construction time to first-use time, which is harder to debug
- Prototype and __init__ side effects- copy.deepcopy bypasses __init__, so any setup logic that lives only in the constructor (e.g. registering with a registry) won't re-run on a clone
- Telescoping constructor smell- More than ~4 optional constructor parameters is the classic signal to switch from a plain constructor to Builder or keyword-only dataclass fields
Singleton is one of the most overused patterns — it introduces hidden global state and makes unit testing harder because you can't easily swap in a test double. Prefer dependency-injecting a single shared instance over hardcoding a Singleton class.