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
Singleton Pattern
is perhaps the most famous creational pattern, but in the world of modern
Python
development, it has become a notorious anti-pattern. Understanding why it fails helps us appreciate better alternatives like the
Object Pool Pattern
.

Prerequisites

To get the most out of this tutorial, you should be comfortable with classes and inheritance in

. Familiarity with
Metaclasses
, decorators, and the concept of
Context Managers
(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,
Python
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}")
Rethinking Creational Patterns: Why Singletons Fail in Python
QUESTIONABLE Object Creation Patterns in Python 🤔

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.

modules are already singletons by nature—importing 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
Database Connections
.

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.

Rethinking Creational Patterns: Why Singletons Fail in Python

Fancy watching it?

Watch the full video and context

3 min read