Summary
Add a ready-made, thread-safe, typed Singleton metaclass to the standard
library so users stop re-deriving the same boilerplate (and its subtle bugs).
Ship an optional Borg (shared-state) metaclass alongside it as a related
utility.
The primary proposal is Singleton. Borg is included because it answers the
obvious follow-up — “what if I want shared state but distinct identities?” —
and because supporting __slots__ for it turns out to be the interesting part.
Motivation
Singletons can be abused — like any pattern — but there are genuine cases where
exactly one instance must exist, constructed lazily and safely: a process-wide
connection pool, a hardware/device handle, a registry, an expensive immutable
config cache. Today every project re-implements the same metaclass, and the
hand-rolled version is usually wrong in at least one of three ways:
- Not thread-safe — a naive
if cls not in _instancescheck races, so two
threads can build two “singletons.” - Untyped — overriding
__call__erases the constructor signature and the
instance type, so everyMyThing()becomesAnyto a type checker. - Breaks on
__slots__— implementations that lean oninstance.__dict__
blow up on slotted classes (not applicable to Singleton).
A blessed implementation gets all three right once, for everyone.
Where it would live
types. It is the stdlib’s home for type-construction machinery and
type-related utilities — it already ships types.new_class,
types.prepare_class, types.resolve_bases, types.DynamicClassAttribute,
SimpleNamespace, and MappingProxyType. A pair of domain-agnostic metaclasses
that alter instantiation belongs next to those: types.Singleton,
types.Borg.
Two alternatives, for completeness:
functools— only ifSingletonis reshaped from a metaclass into a
decorator. Conceptually it is instance memoization, a sibling oflru_cache
andcached_property. The semantics fit; the module’s “operations on
callables” framing is a stretch.abc— where metaclass infrastructure already lives (ABCMeta), so it
is the closest mechanical relative, butabcis specifically about
abstractness, so it would be a category error.
Semantics
A Singleton yields the same instance every time:
foo = MySingleton(...)
bar = MySingleton(...)
foo.value = 123
bar.value = 456
foo.value == bar.value # True
foo is bar # True
A Borg yields distinct instances that share one live state:
foo = MyBorg(...)
bar = MyBorg(...)
foo.value = 123
bar.value = 456
foo.value == bar.value # True
foo is bar # False
Reference implementation
Both are fully working and tested (thread-safety, __slots__, reentrancy, and
typing all covered).
Singleton
from threading import RLock
from typing import Any, ClassVar
class Singleton[**P, T](type):
"""One cached instance per class; works with __dict__ or __slots__ classes."""
_instances: ClassVar[dict[type, Any]] = {}
_lock: ClassVar[RLock] = RLock()
def __call__(cls, *args: P.args, **kwargs: P.kwargs) -> T:
if cls not in cls._instances: # lock-free fast path
with cls._lock:
if cls not in cls._instances: # re-check under lock
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
Borg
Borg shares state by aliasing the storage. For __dict__ classes that is a
one-line reference assignment. For __slots__ classes there is no shareable
container, so on first construction it snapshots the slots into a shared dict and
rewrites each slot into a property backed by that dict; later instances are then
distinct objects whose attribute access routes to the shared dict.
from threading import RLock
from typing import Any, ClassVar
from weakref import WeakKeyDictionary
class Borg[**P, T](type):
"""Distinct instances sharing one live state per class; newest construction wins."""
_states: ClassVar[WeakKeyDictionary[type, dict[str, Any]]] = WeakKeyDictionary()
_lock: ClassVar[RLock] = RLock()
def __call__(cls, *args: P.args, **kwargs: P.kwargs) -> T:
if cls not in cls._states:
with cls._lock:
if cls not in cls._states:
return cls._register(*args, **kwargs)
return cls._overwrite(*args, **kwargs)
def _register(cls, *args: P.args, **kwargs: P.kwargs) -> T:
obj: T = super().__call__(*args, **kwargs)
if cls.__dictoffset__: # has __dict__
cls._states[cls] = obj.__dict__
return obj
names = cls._slot_names(cls) # slotted: rewrite slots
state = {n: getattr(obj, n) for n in names if hasattr(obj, n)}
for name in names:
setattr(cls, name, cls._proxy(state, name))
cls._states[cls] = state
return obj
def _overwrite(cls, *args: P.args, **kwargs: P.kwargs) -> T:
obj: T = super().__call__(*args, **kwargs)
if cls.__dictoffset__:
shared = cls._states[cls]
shared.update(obj.__dict__)
obj.__dict__ = shared
return obj
@staticmethod
def _slot_names(klass: type) -> list[str]:
names: list[str] = []
for base in klass.__mro__:
for name in getattr(base, "__slots__", ()):
if name not in ("__dict__", "__weakref__") and name not in names:
names.append(name)
return names
@staticmethod
def _proxy(state: dict[str, Any], name: str) -> property:
def getter(_obj: Any) -> Any:
try:
return state[name]
except KeyError:
raise AttributeError(name) from None
def setter(_obj: Any, value: Any) -> None:
state[name] = value
return property(getter, setter)
Design decisions
- Thread safety — double-checked locking +
RLock. The unlocked fast path
keeps construction cheap once the instance/state exists; the re-check under the
lock guarantees exactly one first-construction. The lock is reentrant
(RLock) so that a class whose__init__constructs another managed instance
does not self-deadlock. - Typing —
[**P, T]. Making the metaclass generic over aParamSpecand
a returnTypeVar(PEP 695 + PEP 612) is what lets a type checker keep both
the constructor signature and the instance type:MyThing(1, "x")is
checked against__init__and resolves toMyThing, notAny. This is new —
it was not cleanly expressible before — and is itself an argument for shipping
a blessed version now. __slots__support.Singletonis storage-agnostic for free (one cached
object).Borghandles slots via the property-rewrite described above; the
cost is that the slots stop saving memory (state lives in the shared dict),
which is the honest price of live cross-instance sharing.
The remaining Borg choices are policy, not essence, and would be the natural
knobs of a “full” implementation:
- Newest construction wins. Each call runs
__init__and overwrites the
shared state. Alternatives — first-wins (ignore later args) or merge — are
equally defensible defaults. WeakKeyDictionaryforBorg. Keying the registry weakly on the class
lets a dynamically created Borg class be garbage-collected once dropped (its
shared-state dict does not reference the class back). Verified to collect.
Known limitations / open questions
- Metaclass composition (the big one). A metaclass-based
Singleton/Borg
cannot be combined withABCMeta,EnumMeta, or any other metaclass without
a manual metaclass merge. A stdlib version should probably also be offered
as a class decorator (the composable form), or explicitly document the
limitation. This is the strongest objection a core dev would raise. Singletonlifetime. The cached instance back-references its class via
__class__, so aWeakKeyDictionarybuys nothing forSingleton— the
class (and its instance) live for the life of the process. A plaindictis
therefore the honest choice, but “lives forever” should be stated, not
discovered. AWeakValueDictionaryvariant (singleton collected when
unreferenced, then rebuilt) is possible but weakens the identity guarantee and
fails for non-weak-referenceable instances.- “Does Python want a
Singletonat all?” The idiomatic Python singleton is
a module (already process-wide) or a module-level instance, and the GoF
pattern is widely treated as an anti-pattern. The counter-argument: the cases
above genuinely want a class — lazy construction, an enforced identity
invariant, and normal attribute/typing ergonomics — which a bare module does
not give you. - Borg policy bikeshed. Newest-wins vs first-wins vs merge; whether slot
rewriting (a permanent, global side effect of the first instantiation) is
acceptable.
Prior art
- Borg recipe — Alex Martelli, ActiveState; the
original “shared state, distinct identity” idea. functools.lru_cacheon a factory function — a one-liner singleton when
you do not need a class.enum— enum members are already effectively singletons, viaEnumType.- Third-party grab-bags — where utilities like
this conventionally live today.