Rethinking Creational Patterns: Why Singletons Fail in Python
Beyond the Hype of Creational Patterns
Design patterns fall into three buckets: behavioral, structural, and creational. While behavioral patterns like
Prerequisites
To get the most out of this tutorial, you should be comfortable with classes and inheritance in with statement) will help you grasp the more advanced implementation details.
The Problem with Singletons
A
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Logger(metaclass=Singleton):
def log(self, msg):
print(f"Log: {msg}")

In this snippet, we override the __call__ method of a metaclass to intercept instantiation. If the instance exists in our dictionary, we return it; otherwise, we create it. It works, but it’s overkill.
Implementing an Object Pool
The
class ReusablePool:
def __init__(self, size):
self.free = [Reusable() for _ in range(size)]
self.in_use = []
def acquire(self):
if not self.free:
raise Exception("No objects available")
obj = self.free.pop(0)
self.in_use.append(obj)
return obj
def release(self, obj):
self.in_use.remove(obj)
self.free.append(obj)
Safety with Context Managers
Manually calling acquire and release is error-prone. If your code crashes before releasing, you leak resources. We solve this by wrapping the pool in a
class PoolManager:
def __init__(self, pool):
self.pool = pool
def __enter__(self):
self.obj = self.pool.acquire()
return self.obj
def __exit__(self, type, value, traceback):
self.pool.release(self.obj)
Now, the syntax with PoolManager(my_pool) as obj: ensures the object returns to the pool even if an exception occurs. Always remember: when an object returns to the pool, you must reset its state to prevent data leaks between different parts of your application.

Fancy watching it?
Watch the full video and context