A standard Singleton (and related Borg) metaclass

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:

  1. Not thread-safe — a naive if cls not in _instances check races, so two
    threads can build two “singletons.”
  2. Untyped — overriding __call__ erases the constructor signature and the
    instance type, so every MyThing() becomes Any to a type checker.
  3. Breaks on __slots__ — implementations that lean on instance.__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 if Singleton is reshaped from a metaclass into a
    decorator. Conceptually it is instance memoization, a sibling of lru_cache
    and cached_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, but abc is 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 a ParamSpec and
    a return TypeVar (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 to MyThing, not Any. This is new —
    it was not cleanly expressible before — and is itself an argument for shipping
    a blessed version now.
  • __slots__ support. Singleton is storage-agnostic for free (one cached
    object). Borg handles 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.
  • WeakKeyDictionary for Borg. 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

  1. Metaclass composition (the big one). A metaclass-based Singleton/Borg
    cannot be combined with ABCMeta, 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.
  2. Singleton lifetime. The cached instance back-references its class via
    __class__, so a WeakKeyDictionary buys nothing for Singleton — the
    class (and its instance) live for the life of the process. A plain dict is
    therefore the honest choice, but “lives forever” should be stated, not
    discovered. A WeakValueDictionary variant (singleton collected when
    unreferenced, then rebuilt) is possible but weakens the identity guarantee and
    fails for non-weak-referenceable instances.
  3. “Does Python want a Singleton at 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.
  4. 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 recipeAlex Martelli, ActiveState; the
    original “shared state, distinct identity” idea.
  • functools.lru_cache on a factory function — a one-liner singleton when
    you do not need a class.
  • enum — enum members are already effectively singletons, via EnumType.
  • Third-party grab-bags — where utilities like
    this conventionally live today.

Fairly small thing: while I appreciate the fun reference of the name Borg, I think a name such as Collective would probably be more suitable :stuck_out_tongue:

Well, if we want to go for the a more serious name, it is also known as the Monostate pattern. But Borg is equally valid (and conceptually more vivid to explain).

Is the Borg/Mono state pattern really as useful and self-apparent as the references suggests? Why is it an apparent feature to have objects with different identity but shared state?

The closest I found was a reference to “the usual problems” without further explanation.

Where is it a natural and good choice rather than having separate objects refering to a singleton for it’s shared data for example?

That’s a really good question. And maybe I could have been more detailed with examples.

Well, first, allow me a small reframe. Having “separate objects referring to a singleton
for its shared data” isn’t an alternative to Borg — it’s the hand-rolled spelling of Borg.
The idiom people actually write is one line in __init__:

class Thing:
    _shared = {}
    def __init__(self):
        self.__dict__ = Thing._shared   # distinct object, shared state

That’s the whole pattern.
Standardizing it as a metaclass is the same value proposition for Singleton:
people already write this, and they write it subtly wrong – not making
it thread-safe, silently broken under __slots__, no typing (pet peeve).
So the question isn’t really “Borg vs. singleton+references”; it’s “do we
name and fix the shared-state idiom, the same way we’re naming and fixing the
single-instance one.”

Now, your real question: why would you ever want distinct identity with shared
state, rather than just one object?
Well, because sometimes the individual holders
have to be tracked or scoped separately even though they read the same state –
and a singleton, being exactly one immortal object, structurally cannot represent
“several live users of one shared thing.”

Let me bring a simple concrete case: session handles that share an auth token,
where the system wantsto know how many are actually live (weak tracking,
so a dropped handle disappears):

import gc
import weakref

class Session(metaclass=Borg):
    """Borg example for shared authentication state."""
    _live = weakref.WeakSet()
    def __init__(self, token=None):
        if token is not None:           # plain Session() just joins; doesn't reset
            self.token = token
        Session._live.add(self)         # track THIS distinct handle

    @classmethod
    def live(cls):
        """Return the number of live sessions,"""
        return len(cls._live)

api    = Session(token="secret")        # first holder sets the shared token
worker = Session()                      # another holder, no token passed


assert api is not worker                # distinct objects...
assert api.token == worker.token == "secret"   # ...over one shared token
assert Session.live() == 2              # but each is tracked individually

Session(token="rotated")                # re-auth from anywhere; all handles follow
assert api.token == "rotated"

del worker
gc.collect()
assert Session.live() == 1              # liveness reflects reality

A singleton can’t express this. Make Session a singleton and api is worker is
true, so live() is stuck at 1 forever – there is only ever one object, it never
gets collected, and “how many holders are there” becomes unanswerable. The shared
token was easy; the part a singleton loses is the distinct, countable,
collectable identities
sitting in front of it.

That’s the honest shape of it. Same-state is the requirement in both patterns;
the difference is whether the holders need to exist as separate, trackable things
(weakrefs, per-scope lifecycles, distinct dict keys) or collapse into one. When
you genuinely want a single canonical object everyone names and compares with
is, a singleton – or just a module – is the right tool, and Borg would be
overkill. Borg earns its place in the other half: shared state, but the holders
have to stay distinct.

In the end of the day, it all bogs down to the a is b distiction.

a = MyBorg()
b = MyBorg()
d = {a, b} 

Would result in two entries in the set, while only one for some MySingleton.
This ability defines the need.

I would prefer not to have a “class that lies” in the standard library. If you need a lazily created single instance of a class, it can be done without hiding behind the class constructor. That’s a pattern that was fossilized in the Design Patterns book because of Java limitations. There’s no reason to continue it.

You can have lazily created single instances from a function designed to give you one. The code will be more understandable and doesn’t resort to metaclass trickery.

I’m surprised Ned didn’t plug it on his own, but he has a fantastic blog post on why singletons are a bad pattern in Python: Singleton is a bad idea | Ned Batchelder

I’ve been thinking @nedbat I think this is a valid criticism for the Singleton
pattern. After all, one could just as well do something like:

@functools.cache
def settings() -> Settings:
    return Settings(...)

And get back the Singleton, or use one of the other ways described in Ned’s great
blog post. I would say that these do have some issues (for example if arguments
are to be passed onto the constructor, that’s a cache miss if the signature doesn’t
match exactly - this is memoization, not a true singleton).

Even so, and actually going from the blog post

class ChessBoard:
    def __init__(self):
        ...

    @classmethod
    @functools.cache
    def the_board(cls):
        return cls()

While this is a better way to express the singleton in some ways, it’s also not
quite as obvious. One option could be a @singleton class decorator that separates
the concern for a single instance from the class itself.

On the other hand, I would say that most of these criticisms do not quite apply to the
Borg variant – which is why I added it. A new Borg does not lie. It serves to address
that. It is another instance altogether. It just points to a shared state.

What Borg shares is state, and sharing state is a different kind of thing from
lying about construction. Redefining your own type’s state semantics – explicitly,
per class, opt-in right there in the class statement is not the same as violating
the universal “T() is a fresh T” contract that holds for everything else.
A shared-state class is no more “lying” than any class with a deliberate __init__.
Independent state between instances is not a language guarantee — Python
already lets classes customize it. Nobody calls a class with items=[] shared
at class level a “liar”

And the is operator actually shows that there is no lie. They share the state – but
crucially not in identity. is tells the truth. It reports two distinct objects,
because there are two distinct objects.

Where I think there’s a need for reconsidering is on if Borg should be a metaclass
or some other construct. Making it a regular class does offer advantages like allowing
it to be a Mixin with other classes

class Session(ABC, Borg)

While actually providing a better default implementation that avoids the main criticisms
of a Singleton. If Singleton is bad, why not provide good alternatives out of the box?

You could argue python actually uses singletons more than most languages. Every module is a singleton in more or less everyway. That’s still an argument for using those instead of implementing another variant of it.

As for the Borg pattern

Why would you need two objects with shared data in the same set where those objects could just reference a singleton instead of sharing their internal namespace dict? Why is that ever clearer or better?

Also large parts of the above feels very heavily AI written. There’s enough thought that I feel it’s not necessarily completely dumped from AI but please state your own thoughts clearly first and if you think a large AI explanation helps mark it as such below, maybe collapsed.

What’s the point of them being separate though? To what extent is a new Borg relevantly not a singleton?

If I need a singleton, I usually use a module-level variable. The import machinery will ensure that the module is shared correctly. [1]


  1. Assuming we’re not doing reload shenanigans. But if I’m doing hot reloads, I think very differently about singletons and such. ↩︎

There are a few reasonable cases for singletons, but I don’t think these need standardization or making them any easier to create. Them requiring extra thought and not being something that the language “makes easy” gives appropriate time to consider if any of the alternative designs that are often better are better in a specific case.

Well, there are some issues with the singleton, as correctly pointed above by @nedbat and argued very well in the blog post linked above.

The two objects in the same set was just a very trivial example, but the reason we may need it is to keep count of live instances like in the example. My goal really was to demonstrate that these point to different instances.

I did use it to format the md itself and to clean up any typos, not for the ideas or the code.
And I did go through all changes after. I will keep the advice in mind, since the goal in
LLM usage as I see it is to make clearer text, not to make it worse.

I guess this counts as a usecase, but I still don’t understand why you would want to do this. Having “token” be a global/threadlocal/contextvar variable somewhere is also perfectly functional - why mutate the Session object into such a bizarre configuration? Another very simple alternative is to have a Token class that only has a single attribute - the token that is currently active. This then gets feed into the Session instances. Or a dozen different variations that don’t require you to construct objects that behave in a surprising way.

Well, fundamentally, each instance is its own thing, its own memory address.
What they share is the pointer to the __dict__ or __slots__.

So it does address the criticism of Singletons lying. A Borg does not lie. A new Borg is a new instance, an instance that points to a shared address in memory.

I think that many usecases for Singletons are handled by module-level variables. Some aren’t. And some should stop being Singletons all together.

I am inclined to agree at least partially after all that I’ve read so far in this thread for Singletons.
But I’m also inclined to think that a Monostate pattern might be useful as a better alternative to it.

One of the alternative design decisions is:

Do you need a true Singleton instance, or just single shared state across instances?

If the answer is the second, then a Borg would be the kosher fix.

I know that they are, definitionally, separate instances. What I want to know is how this is relevant. If they all share state, how are they usefully different?

For a comparison, integers and floats in Python have no distinguishable internal state other than their value. So there’s no purpose to having multiple instances, and they can be shared as needed. (Current versions of CPython cache small integers but not floats; however, within a single compilation unit, any ints or floats with the same value will reuse the same instance.)

When is it that you truly need distinct objects but entirely shared internal state?

Multiple ways can be used for the same goal. This was a way for a particular one,

Well, if you see it in a different way, this is a flyweight pattern. You have a bunch of instances in many places but they only take one[1] space in memory, and can be changed all at once.

I started looking into it while thinking about the one-electron_universe, when applied to references in asts


  1. well, not quite ↩︎

Yes, but apparently you think this particular way has some benefits that make it worth supporting in the language. If you want this implemented, you are going to have to convince others that these benefits are worth the costs. I am not really in the set of people you need to convince, but you should make at least some attempt of making arguments.

Honestly, I like the elegance it brings.

I don’t quite like the solution of instantiating the object, because then you run into the same
type of issue where you expect MyClass | None and then you either have to fake
it with a __call__ in the class (which doesn’t work for a @final) or some extra logic.
And I liked the idea of saving memory in complex data structure where Items may be
repeated and put together, but are still unique references.

But to be very honest, It’s not about making some attempt of making arguments.
I did think the idea was good. In some ways I still do. But there are some very valid
criticisms of the Singleton idea (I might eventually revisit the Borg one).

Overall though, it does feel like the community opinion is very much against it.
So I apologize if the comment was dismissive. It wasn’t meant to be.

I will ask you the other way around. You said you’re not in the set of people I need to convince.
What are your thoughts then?