Singleton¶
Intent¶
Guarantee that a class has exactly one instance and give every caller a known way to reach it. Two problems hide in that sentence: a resource that is genuinely singular (one registry of loggers, one device handle, one settings object) and a construction that must happen at most once even when several threads ask at the same moment.
When to use and when not to¶
Use it when
- Two instances would be a bug, not a waste: a registry keyed by name, a process-wide metrics sink, the handle to one piece of hardware.
- Callers are many and scattered with no composition root to thread an object through; library code such as
logginglives here. - You are wrapping a framework that already works this way and want one access path rather than two.
Leave it out when
- "There is one of them today." One parking lot, one elevator controller, one scheduler: that is one instance by convention, built in
mainand injected, so a second lot is a second object and a test builds its own. - The object holds mutable state that tests need fresh. The
resethook appears, then the test-ordering bug. - What you want is global access, not single existence. That is a global variable wearing a class; pass the object instead.
- You are counting on "one" across processes. A Singleton is per interpreter: a server with eight worker processes has eight of them.
Structure¶
The classic form keeps its instance in a class attribute; the metaclass form moves that logic out of the class; the injected form does not enforce anything.
classDiagram
class ConfigManager {
-_instance: ConfigManager
-_lock: Lock
-_guard: RLock
-_settings: Dict~str,str~
+__new__(cls) ConfigManager
+get(key: str) str
+set(key: str, value: str) None
+load(values: Mapping~str,str~) None
+snapshot() Dict~str,str~
+reset() None
}
class SingletonMeta {
<<metaclass>>
-_instance: Any
-_lock: Lock
+__call__(args, kwargs) Any
}
class FeatureFlags {
+flags: Dict~str,bool~
+enabled(name: str) bool
}
class Borg {
-_shared_state: Dict~str,Any~
}
class AppConfig {
+region: str
+max_retries: int
}
class Uploader {
-_config: AppConfig
+describe() str
}
SingletonMeta ..> FeatureFlags : instantiates once
Uploader o-- AppConfig : injected
_instance and _lock are class-level in both forms. ConfigManager does the work in __new__; SingletonMeta intercepts the call before __new__ or __init__ run, so FeatureFlags is an ordinary class. Borg shares state, not identity. Uploader is the contrast: it takes an AppConfig and never asks whether another exists.
Canonical example in Python¶
The classic form first (code/patterns/singleton.py, tested by code/patterns/tests/test_singleton.py):
class ConfigManager:
"""The classic form: ``__new__`` returns the one instance, guarded by a lock.
Two locks, two jobs: ``_lock`` (class level) protects ``_instance`` during
creation; ``_guard`` (instance level) protects ``_settings`` afterwards. There
is deliberately no ``__init__``: Python runs ``__init__`` on *every*
``ConfigManager()`` call, so state set up there would be wiped each time the
access point is used. ``_init_once`` runs under the lock, exactly once.
"""
_instance: ClassVar[ConfigManager | None] = None
_lock: ClassVar[threading.Lock] = threading.Lock()
def __new__(cls) -> ConfigManager:
if cls._instance is None: # fast path: no lock once the instance exists
with cls._lock:
if cls._instance is None: # second check: the loser of the race stops here
instance = super().__new__(cls)
instance._init_once()
cls._instance = instance # published last, so readers see a finished object
return cls._instance
def _init_once(self) -> None:
self._guard = threading.RLock()
self._settings: dict[str, str] = {}
def get(self, key: str) -> str:
with self._guard:
try:
return self._settings[key]
except KeyError:
raise NotFoundError(f"no setting named {key!r}") from None
def set(self, key: str, value: str) -> None:
with self._guard:
self._settings[key] = value
def load(self, values: Mapping[str, str]) -> None:
with self._guard:
self._settings.update(values)
def snapshot(self) -> dict[str, str]:
with self._guard:
return dict(self._settings)
@classmethod
def reset(cls) -> None:
"""Test hook: forget the instance. Needing this is the first sign injection would be simpler."""
with cls._lock:
cls._instance = None
Three decisions to say out loud:
__new__, never__init__. Python calls__init__on everyConfigManager()call, including the ones where__new__handed back the existing object, so state set up there would be wiped on each access._init_onceruns under the lock, exactly once, and the class has no__init__at all.- Check, lock, check again, publish last. The first check skips the lock on every call after the first; the second stops the thread that lost the race; assigning
cls._instanceafter_init_oncemeans no reader sees a half-built object. The lock itself is cheap (an uncontended mutex is about 17 ns on the latency ladder: 100k accesses a second spend 100,000 x 17 ns = 1.7 ms in it), so the fast path is about contention during a burst of first calls, not speed. - Two locks, two jobs.
_lock(class level) protects creation;_guard(instance level, re-entrant) protects_settings. Say which lock covers what:setcannot race with__new__because it needs an instance that only a finished__new__returns.
reset exists because the tests needed it. That is the tell: a Singleton meeting a test suite grows a back door, and every test becomes order-dependent.
Running python -m patterns.singleton prints:
--- classic: every ConfigManager() call returns the same object ---
first is second: True; second.get('region') -> eu-west-1
--- 16 threads race to create the first instance ---
distinct instances: 1
--- metaclass: __init__ runs once, later arguments are ignored ---
flags is again: True; beta enabled: True
--- cached factory and Borg ---
default_config() is default_config(): True
left is right: False; right.theme: dark
--- injection: one instance by convention, built in main ---
upload to eu-west-1 with 5 retries
a test builds its own: upload to local with 0 retries
Pythonic variant¶
The shortest Singleton in Python is a module. import runs it once and caches it in sys.modules, so a module-level object is created once and shared by every importer, with no lock and no __new__:
# settings.py: the module is the singleton; import caches it in sys.modules
from patterns.singleton import AppConfig
config = AppConfig(region="eu-west-1")
# anywhere else
from settings import config
Three more forms, each fixing one weakness of the classic:
class SingletonMeta(type):
"""Metaclass form: ``Cls()`` is intercepted before ``__new__`` and ``__init__`` run.
``__init__`` therefore runs exactly once, and every class built with this
metaclass gets its own ``_instance`` and ``_lock``: a subclass is a second
singleton, not a second copy of the parent.
"""
def __init__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]) -> None:
super().__init__(name, bases, namespace)
cls._instance = None
cls._lock = threading.Lock()
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__call__(*args, **kwargs)
return cls._instance
class FeatureFlags(metaclass=SingletonMeta):
"""Constructor arguments after the first call are silently ignored: a known trap."""
def __init__(self, flags: Mapping[str, bool] | None = None) -> None:
self.flags = dict(flags or {})
def enabled(self, name: str) -> bool:
return self.flags.get(name, False)
@dataclass(frozen=True, slots=True)
class AppConfig:
"""A frozen value: safe to share, impossible to corrupt, trivial to build twice in tests."""
region: str = "us-east-1"
max_retries: int = 3
@cache
def default_config() -> AppConfig:
"""The function is the access point and ``cache`` is the instance store.
``cache`` holds no lock around the call: two threads racing on the very first
call can each build an ``AppConfig`` and the cache keeps the last one. Harmless
for a frozen value, wrong for anything that owns a connection or a thread.
"""
return AppConfig()
class Borg:
"""Monostate: many instances, one shared ``__dict__``. Identity differs, state agrees."""
_shared_state: ClassVar[dict[str, Any]] = {}
def __init__(self) -> None:
self.__dict__ = self._shared_state
- Metaclass.
SingletonMeta.__call__runs before__new__and__init__, so__init__executes once and the class body stays clean. Each class gets its own_instance: a subclass is a second singleton, where the__new__form would hand a subclass its parent's object. The trap: later constructor arguments are silently ignored. @cacheon a factory. The function is the access point and the cache is the store. No lock guards the first call, which is harmless for a frozenAppConfigand wrong for anything owning a connection or a thread.- Borg. Many instances, one
__dict__: identity differs, state agrees. Know it; it is global mutable state with extra steps.
The answer this handbook prefers enforces nothing:
class Uploader:
"""Takes its configuration as a constructor argument.
Nothing here enforces that only one ``AppConfig`` exists: ``main`` builds one
and passes it along, and a test builds a different one. One instance by
convention rather than by construction is this handbook's default answer.
"""
def __init__(self, config: AppConfig) -> None:
self._config = config
def describe(self) -> str:
return f"upload to {self._config.region} with {self._config.max_retries} retries"
main builds one AppConfig and passes it to Uploader; a test builds a different one. Nothing enforces uniqueness and nothing needs to: the shared object was the point, never the guarantee.
| Reach for | When |
|---|---|
| A module-level instance | Immutable configuration or a stateless helper |
| Constructor injection | Mutable state, anything a test wants fresh; the default |
__new__ with a lock or a metaclass |
Construction at most once under concurrency, with no composition root |
@cache on a zero-argument factory |
A lazily built immutable value |
Real-world usage¶
logging.getLogger(name)is the pattern done well: oneManagermaps names to loggers and creates them underlogging._lock, so the same name always yields the same logger. The registry is singular; the loggers are many.sys.modulesmakes every module a singleton, which is why Django'sdjango.conf.settingsis a module-levelLazySettingsobject that configures itself on first attribute access.- Interned values.
None,True,False,Ellipsisand the empty tuple are singletons;x is Noneis correct only because of that guarantee. functools.cacheon a factory is the stdlib's one-line memoised constructor, as indefault_configabove.
Related patterns and confusions¶
| Looks like Singleton | How to tell them apart |
|---|---|
| Dependency Injection | The alternative, not a relative: every collaborator gets the same object without any class enforcing it, and the enforcement is what makes Singletons hard to test. |
| Module | Python's native Singleton: one object per import, no lock needed because the import system serialises module execution. |
| Borg (Monostate) | Same state, different identities; a is b is false, a.x == b.x is true. |
| Flyweight | Many objects sharing intrinsic state, not one object; interned strings and Enum members are flyweights. |
| Object Pool | One becomes N: a bounded set of reusable instances with acquire and release. The pool is often the only Singleton a system needs. |
| Factory Method | A factory decides which class to build; a Singleton access point decides whether to build at all. getLogger is both. |
Where it appears in LLD problems¶
- Design a parking lot — deliberately not used: the lot is built in
mainand injected into the gates, and saying so is worth more in the room than the pattern. - Design an elevator system — one controller per building tempts a Singleton; it is a Mediator built once and handed to the cars.
- Design a vending machine (and a coffee machine) — one machine by convention; the tests build dozens.
- Design a logging framework — the legitimate case: a
LogManagerregistry returning the same logger for the same name, as the stdlib does. - Design a task scheduler (cron, LLD) — a process-wide scheduler, injected with its
Clockso tests can move time by hand.
Interview tips¶
Interview tip
Give the three-sentence answer before you are asked: "ConfigManager is a Singleton, __new__ with double-checked locking and no __init__; in Python I would write it as a module-level instance; here I would build one in main and inject it, because the tests want their own." Naming the thread-safe form proves you can write it; choosing not to proves judgement.
Common mistake
Putting state in __init__. It runs on every call, so the second ConfigManager() silently resets the first one's settings and the bug shows up as "config sometimes empty" far from the cause. Runner-up: believing the guarantee survives fork; one instance per process means eight instances under eight workers.
Related¶
- Dependency Injection — the default answer when "one instance" is a convention
- Concurrency for LLD in Python — locks, the GIL and why double-checked locking holds
- Object Pool — one instance becomes a bounded set
- Design a parking lot — a Singleton rejected on purpose
- Design a logging framework — the registry that earns the pattern
- Gamma, Helm, Johnson and Vlissides, Design Patterns (1994), Singleton
- Python documentation:
logging—getLogger - Python documentation: Data model —
object.__new__