Skip to content

Instantly share code, notes, and snippets.

@outofmbufs
Last active October 22, 2025 17:45
Show Gist options
  • Save outofmbufs/0c6bcab45c14f4cf83895ea114f97449 to your computer and use it in GitHub Desktop.
Save outofmbufs/0c6bcab45c14f4cf83895ea114f97449 to your computer and use it in GitHub Desktop.
Yet Another python singletons implementation
# MIT License
#
# Copyright (c) 2025 Neil Webber
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from threading import Lock
# SUPPORT FOR MAKING THREAD-SAFE SINGLETON CLASSES
#
# Arguably all this generalized code is overly complicated versus simply
# making the application-specific class enforce singleton-ness itself.
# On top of that, there are some fundamental limitations involving
# subclassing and class-level keyword arguments for __init_subclass__()
# Viewer/programmer discretion advised.
#
# Three ways are provided to make thread-safe singleton classes:
#
# * SingletonMeta -- use as a metaclass
# * Singleton -- subclass this
# * singleton -- use as a class decorator
#
# METACLASS technique:
#
# Specify SingletonMeta as a metaclass. Example:
#
# from singletons import SingletonMeta
# class Foo(metaclass=SingletonMeta):
# pass
#
# f1 = Foo()
# f2 = Foo()
#
# f1 is f2 --> True
#
# SUBCLASS technique:
#
# Under the covers this is the same as specifying SingletonMeta as
# a metaclass, but hides that voodoo:
#
# from singletons import Singleton
# class Foo(Singleton)
# ... behaves the same as the METACLASS example above
#
# DECORATOR technique:
#
# Even more hiding of details, this hides the subclassing and the metaclass:
#
# from singletons import singleton
# @singleton
# class Foo:
# pass
#
# ---------------
# ABOUT PARAMETER CHECKING ON 2nd, 3rd, ... (non)INSTANTIATIONS:
#
# Because subsequent Foo() invocations do not create a new object,
# no underlying __new__ or __init__ calls are made. Consequently,
# there is no argument checking on those calls. Thus:
#
# class Bar(metaclass=SingletonMeta):
# def __init__(self, x):
# self.x = x
#
# b = Bar(1, 2)
# b = Bar()
#
# These calls will both fail because Bar initialization takes one argument.
# However, after a successful instantiation:
#
# b = Bar(12)
#
# subsequent malformed calls will SUCCEED:
#
# b1 = Bar(1, 2)
# b2 = Bar()
#
# because they simply return the already-instantiated singleton object, and
# do not invoke __new__ or __init__.
#
# If required/desired, a class can provide a singleton_validator() method
# with this signature:
#
# # ... validate args/kwargs, raise TypeError if bad
# def singleton_validator(self, *args, **kwargs)
# pass
#
# The method should raise TypeError if the args/kwargs are not acceptable.
# One built-in validator, singleton_sameargs, is supplied that will raise
# a TypeError if the subsequent arguments don't match the originals:
#
# @singleton
# class Foo:
# singleton_validator = SingletonMeta.singleton_sameargs
# def __init__(self, x):
# self.x = x
#
# f1 = Foo(1)
# f2 = Foo(2) --> this will raise TypeError because argument x changed
#
class SingletonMeta(type):
"""Metaclass to force any class to be a singleton."""
# __new__ is invoked when the CLASS using SingletonMeta as a metaclass
# is created. That class will be modified:
# - It gets an attribute, __instance, to record the single object.
# - It gets a lock, __instance_lock, for thread safety.
#
# Note that the dunder name-mangling becomes a _SingletonMeta__ prefix
# in the underlying (not SingletonMeta) cls and so there should not
# be any (realistic) possibility of name conflicts.
def __new__(cls, name, bases, dict_, **kwargs):
newclass = super().__new__(cls, name, bases, dict_, **kwargs)
newclass.__instance = None
newclass.__instance_lock = Lock()
return newclass
# this (no-op) singleton args validator is the default if the class
# does not override it
@staticmethod
def singleton_validator(instance, *args, **kwargs):
"""This validator accepts all args/kwargs no matter what."""
pass
@staticmethod
def singleton_noargs(instance, *args, **kwargs):
"""This validator enforces there are no args and no kwargs."""
if args or kwargs:
raise TypeError(
f"{instance.__class__.__name__}() takes no arguments")
@staticmethod
def singleton_sameargs(instance, *args, **kwargs):
"""This validator enforces 'matching' arguments, as defined here."""
created_args, created_kwargs = instance.__class__.__akw
if args != created_args or kwargs != created_kwargs:
raise TypeError(
f"{instance.__class__.__name__}() arguments mismatch")
# heart of __call__ ... this either returns the singleton if it
# already exists, or causes it to be instantiated. There are two
# return values: the object, and whether it was just made (boolean)
def __create_singleton(klass, *args, **kwargs):
with klass.__instance_lock:
# there is a race outside the lock in __call__,
# so check again here inside the lock. The point of the
# outside the lock test is that it (safely) avoids lock overhead
# in all cases once the singleton has been instantiated.
if klass.__instance is not None:
return klass.__instance, False
# nope, genuinely need to make the instance.
# Note tht args are saved for singleton_sameargs
klass.__akw = (args, kwargs)
klass.__instance = super().__call__(*args, **kwargs)
return klass.__instance, True
# This gets called when the class using SingletonMeta as a metaclass
# gets called to create a new class object. Note that 'klass' in the
# code below is that class, NOT SingletonMeta. The goal here is to
# make the singleton object ONLY if necessary, and either way return it.
def __call__(klass, *args, **kwargs):
# Note: Overhead of locking is substantial; fortunately it can be
# safely avoided in this test for the (common, dominant) case
# of not being the first time(s) through.
if klass.__instance is not None:
instance, first = klass.__instance, False
else:
# (probably) have to make it ... see test again inside lock though
instance, first = klass.__create_singleton(*args, **kwargs)
# When the singleton is first instantiated, __new__ or __init__
# in the class itself (not this metaclass) can validate args/kwargs.
# But when a previously-instantiated singleton is simply returned,
# they don't get that chance. If the class wants to validate the
# args/kwargs in that case too, it must define a singleton_validator
# method, which will be invoked as shown in the code below.
# The singleton_validator should raise TypeError if it does not
# like the args/kwargs given. If no singleton_validator is defined
# the __new__ logic above ensures the null ("everything is ok")
# validator is set up (so there is always a singleton_validator)
if not first:
klass.singleton_validator(instance, *args, **kwargs)
return instance
# This is how any class can be made into a singleton, by invoking
# the _SingletonMeta as its metaclass. The point of doing that here
# with this stub class is other classes can just derive from Singleton,
# rather than having to say "metaclass=_SingletonMeta).
class Singleton(metaclass=SingletonMeta):
pass
# This is a decorator uses SingletonMeta as the metaclass (instead of type)
# Usage:
# @singleton
# class Foo:
# pass
#
# CAUTION: This creates the decorated class twice - which could have
# implications. In particular, the second creation of the class
# has no way to know what __init_subclass__ style kwargs the
# original class might have used, and passes no such args when
# re-creating the class (to have a SingletonMeta metaclass).
#
# IF __init_subclass__ style class-declaration kwargs are being used,
# do not use the decorator; use explicit subclassing or metaclass.
#
def singleton(klass):
return SingletonMeta(klass.__name__, (klass,), {})
#
# TESTS
#
if __name__ == "__main__":
import unittest
import threading
from contextlib import nullcontext
class TestMethods(unittest.TestCase):
# METACLASS test: taken directly from comments at top of file
def test_metaclass(self):
class Foo(metaclass=SingletonMeta):
pass
f1 = Foo()
f2 = Foo()
self.assertTrue(f1 is not None)
self.assertTrue(f1 is f2)
# SUBCLASS test: inspired by comments at top of file
def test_subclass(self):
class Foo(Singleton):
pass
f1 = Foo()
f2 = Foo()
self.assertTrue(f1 is not None)
self.assertTrue(f1 is f2)
# DECORATOR test: inspired by comments at top of file
def test_decorator(self):
@singleton
class Foo:
pass
f1 = Foo()
f2 = Foo()
self.assertTrue(f1 is not None)
self.assertTrue(f1 is f2)
# subclassING test, using SingletonMeta metaclass
def test_subclassing1(self):
class Foo(metaclass=SingletonMeta):
pass
class Bar(Foo):
pass
f1 = Foo()
f2 = Foo()
self.assertTrue(f1 is not None)
self.assertTrue(f1 is f2)
b1 = Bar()
b2 = Bar()
self.assertTrue(b1 is not None)
self.assertTrue(b1 is b2)
self.assertTrue(b1 is not f1)
# subclassING test, using Singleton
def test_subclassing2(self):
class Foo(Singleton):
pass
class Bar(Foo):
pass
f1 = Foo()
f2 = Foo()
self.assertTrue(f1 is not None)
self.assertTrue(f1 is f2)
b1 = Bar()
b2 = Bar()
self.assertTrue(b1 is not None)
self.assertTrue(b1 is b2)
self.assertTrue(b1 is not f1)
# test that class-level keyword args make it into __init_subclass__
def test_clskw1(self):
class Base:
def __init_subclass__(cls, /, clown, **kwargs):
cls.clown = clown
class SubSB(Singleton, Base, clown='bozo'+'SubSB'):
pass
class SubBS(Base, Singleton, clown='bozo'+'SubBS'):
pass
class SubMC(Base, metaclass=SingletonMeta, clown='bozo'+'SubMC'):
pass
for kls in (SubSB, SubBS, SubMC):
xname = 'bozo'+kls.__name__
self.assertEqual(kls.clown, xname)
s1 = kls()
s2 = kls()
self.assertEqual(s1.clown, xname)
self.assertEqual(s2.clown, xname)
self.assertTrue(s1 is s2)
# test that tests the locking and proves the racing if no locks
def test_race(self):
def makeX():
"""Create the test class X"""
class X(Singleton):
def __init__(self, id):
myattrname = f"ID_{id}"
for j in range(100):
for i in range(100000):
pass
setattr(X, myattrname, id)
return X
# the guts of the test - start all the threads, wait for them
def _race(xk):
threads = [
threading.Thread(target=xk, args=(t_id,))
for t_id in range(100)]
for t in threads:
t.start()
for t in threads:
t.join()
# Step 1: test the test ... monkey patch out the locking
# and see if the test produces race failures.
xk = makeX()
setattr(xk, '_SingletonMeta__instance_lock', nullcontext())
_race(xk)
# since there was no locking and there were significant delays
# in the __init__ method, multiple threads should have
# been in the __init__ logic and created ID_nnnnn attrs.
ids = [a for a in dir(xk) if a.startswith('ID_')]
# this implies more than one thread instantiated the object
self.assertTrue(len(ids) > 1)
# now the same thing without monkey patching and there
# should only be one ID_ attribute created
xk = makeX() # note NEW class so no patches carried over
_race(xk)
ids = [a for a in dir(xk) if a.startswith('ID_')]
self.assertEqual(len(ids), 1)
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment