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 manage communication and structural patterns handle assembly, creational patterns dictate how we instantiate objects. The is perhaps the most famous creational pattern, but in the world of modern development, it has become a notorious anti-pattern. Understanding why it fails helps us appreciate better alternatives like the .
Prerequisites
To get the most out of this tutorial, you should be comfortable with classes and inheritance in . Familiarity with , decorators, and the concept of (the with statement) will help you grasp the more advanced implementation details.
The Problem with Singletons
A restricts a class to a single instance. While this sounds useful for loggers or database managers, it creates a global state that makes testing nearly impossible. Because you cannot easily reset the instance between test runs, your tests become coupled. Furthermore, lacks private constructors. Implementing a true singleton requires "trickery" like metaclasses:
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鈥檚 overkill. modules are already singletons by nature鈥攊mporting a module multiple times always returns the same object. Just use a module.
Implementing an Object Pool
The is a smarter generalization. Instead of one instance, it manages a cache of reusable objects. This is vital when object creation is expensive, such as maintaining .
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.
- 31%路 products
- 15%路 products
- 15%路 products
- 15%路 products
- 8%路 products
- Other topics
- 15%

QUESTIONABLE Object Creation Patterns in Python 馃
WatchArjanCodes // 15:10
On this channel, I post videos about programming and software design to help you take your coding skills to the next level. I'm an entrepreneur and a university lecturer in computer science, with more than 20 years of experience in software development and design. If you're a software developer and you want to improve your development skills, and learn more about programming in general, make sure to subscribe for helpful videos. I post a video here every Friday. If you have any suggestion for a topic you'd like me to cover, just leave a comment on any of my videos and I'll take it under consideration. Thanks for watching!