Creational Patterns I: Singleton & Factory Method

Design Patterns

Chapter 2 · Creational Patterns I: Singleton & Factory Method

Creational patterns control how objects get created. This chapter covers two of the most common — and Singleton, one of the most commonly misused — with real pitfalls reproduced directly, not just described.

Singleton: Guaranteeing Exactly One Instance

Ordinarily, calling a class's constructor twice gives two separate objects:

class Logger: def __init__(self): self.messages = [] a = Logger() b = Logger()
Verified directly
a is b evaluates to False — two genuinely separate objects, with different memory addresses.

Singleton overrides object creation itself so every call returns the same object:

class SingletonLogger: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance.messages = [] return cls._instance
Verified directly
x = SingletonLogger() and y = SingletonLogger(): x is y evaluates to True — identical memory address both times.

Pitfall 1: Singleton State Silently Pollutes Later Code

A shared global instance means shared global state — and that state doesn't reset itself between unrelated uses.

Verified directly — a genuine, reproduced test-pollution bug
A SingletonCounter with an increment() method: a first routine increments it three times and correctly finds count == 3. A second, entirely independent routine — written expecting a fresh counter — calls SingletonCounter(), increments once, and expects count == 1. It actually gets count == 4: the leftover state from the first routine never went away, because both routines received the exact same object.
Why this is a genuinely common real bug, not a contrived one
This is precisely the mechanism behind flaky, order-dependent test suites — a test that passes in isolation fails only when run after another specific test, because a Singleton somewhere in the codebase silently carried state between them. The bug isn't in either routine's own logic; it's in the shared object neither routine realized it didn't have exclusive ownership of.

Pitfall 2: A Naive Singleton Isn't Thread-Safe

The lazy-initialization check, if cls._instance is None:, has a real gap: two threads can both evaluate that check as True before either one finishes creating the instance.

Verified directly — a real, reproduced race condition
Launching 20 threads simultaneously, each calling a naive lazy Singleton (with a small artificial delay inserted between the check and the creation, to widen the race window): instead of 1 shared instance, 8 genuinely distinct instances were created — confirmed by comparing each thread's own object identity. The "only one instance" guarantee failed completely under real concurrent access.
Verified directly — the standard fix, confirmed working
Adding a lock with double-checked locking — check, acquire a lock, check again, then create — and re-running the identical 20-thread test: exactly 1 distinct instance, every time.

A Python-Specific Note: You Often Already Have a Singleton

Verified directly — Python's own module system is a built-in Singleton
import os followed by import os as os2: os is os2 evaluates to True. Python caches every imported module in sys.modules, so importing the same module anywhere, any number of times, always returns the identical object — a real, verified, zero-code Singleton, already built into the language. A module-level instance (a plain object created once at the top of a module) is often the more idiomatic Python choice over a hand-rolled __new__-based class, for exactly this reason.

Factory Method: Delegating "Which Class?" to a Subclass

Factory Method defines a creation method in a base class without deciding what it creates — each subclass overrides that one method to supply a different concrete type, while every other method in the base class works entirely through the shared interface.

class NotificationCreator(ABC): @abstractmethod def create_notification(self): pass # the factory method itself def notify(self, message): notification = self.create_notification() return notification.send(message) class EmailNotificationCreator(NotificationCreator): def create_notification(self): return EmailNotification() class SMSNotificationCreator(NotificationCreator): def create_notification(self): return SMSNotification()
Verified directly
EmailNotificationCreator().notify('Your order shipped') returns 'Email sent: Your order shipped'; SMSNotificationCreator().notify(...) returns 'SMS sent: Your order shipped' — the identical notify() method, defined once in the base class, correctly produces different behavior depending purely on which subclass's own create_notification() ran.
Verified directly — the same extensibility payoff as Chapter 1
Adding an entirely new PushNotificationCreator — a new subclass with its own create_notification() returning a new PushNotification class — works correctly ('Push sent: Your order shipped') with zero changes to NotificationCreator.notify() itself, exactly the payoff Chapter 1 first demonstrated.

Where This Connects

This chapter's findingWhat it sets up
Singleton's verified test-pollution and race-condition failuresA concrete case study for Clean Code, SOLID & Refactoring's own treatment of hidden global state and testability
Factory Method's zero-change extensibility, verifiedThe same shape reappears at a larger scale in Chapter 3's Abstract Factory
Double-checked locking as a real, verified fixA concrete instance of the "shortcuts need a checkable justification" discipline pseudocode1's own Chapter 7 established for greedy algorithms

Hands-On Exercises

Exercise 1

Using this chapter's own SingletonCounter example, explain what value a third, independent routine would see if it called SingletonCounter() and read count immediately, without calling increment() at all, after both routines from this chapter's own example have already run.

📄 View solution
Exercise 2

Using this chapter's own verified thread-safety findings, explain in your own words why the naive Singleton's race condition specifically requires MULTIPLE threads calling it for the very first time (before any instance exists yet) — and why calling an already-fully-initialized Singleton from multiple threads afterward would not have the same problem.

📄 View solution
Exercise 3

Using this chapter's own Factory Method example, write a new SlackNotification class and a SlackNotificationCreator subclass, following the same pattern as EmailNotification/EmailNotificationCreator. Verify it works correctly through the existing, unmodified notify() method.

📄 View solution

Chapter 2 Quick Reference

  • Singleton: a class that guarantees exactly one instance — verified via a __new__ override, x is y confirmed True
  • Verified pitfall 1: shared Singleton state silently pollutes later, unrelated code — a second routine's "fresh" counter unexpectedly started at 3 instead of 0
  • Verified pitfall 2: a naive lazy Singleton is not thread-safe — 20 concurrent threads produced 8 distinct instances instead of 1; double-checked locking fixed it to exactly 1
  • Verified: Python's own module system (sys.modules) is already a built-in Singleton — often the more idiomatic choice over a hand-rolled class
  • Factory Method: a base class delegates "which concrete type?" to an overridden method in each subclass — verified correct behavior and zero-change extensibility, mirroring Chapter 1's own findings
  • Next chapter: Creational Patterns II — Abstract Factory and Builder