Create Singleton Class with a Decorator in Python: Complete Design Pattern Implementation Guide

Create singleton class with a decorator in python

I resisted the singleton pattern for a long time because most of what I’d read about it was warnings — global state in disguise, hard to test, a smell more than a solution. All of that is fair criticism. But then I ran into a genuinely legitimate use case (a single shared connection pool for a small service) and had to actually implement one properly. What I learned is that Python gives you several ways to build a singleton, and a decorator-based approach is one of the cleanest — more explicit than metaclasses, more reusable than a module-level instance. This guide covers why and how, along with the tradeoffs of each approach.

What a Singleton Is and Why You’d Want One

A singleton is a class that only ever allows exactly one instance to exist — every attempt to create a new instance returns the same object instead. Legitimate use cases include configuration managers, logging setups, connection pools, and caches where having two independent instances would cause inconsistency or wasted resources.

class ConfigManager:
    pass

a = ConfigManager()
b = ConfigManager()
print(a is b)  # False, by default — every call to ConfigManager() makes a new object

By default, Python makes a fresh object every time you call a class. A singleton pattern intercepts that and forces reuse.

The Decorator Approach

The core idea: wrap the class itself in a decorator that intercepts instantiation, checks if an instance already exists, and returns it if so.

import functools

def singleton(cls):
    instances = {}

    @functools.wraps(cls, updated=[])
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return get_instance


@singleton
class ConfigManager:
    def __init__(self):
        print("Creating ConfigManager instance")
        self.settings = {}


a = ConfigManager()
b = ConfigManager()

print(a is b)  # True
# Output printed only once: "Creating ConfigManager instance"

instances is a dictionary captured in the closure of get_instance, keyed by the class (cls) — this makes the decorator reusable across multiple different classes without them stepping on each other’s state, since each decorated class gets its own entry:

@singleton
class Logger:
    def __init__(self):
        print("Creating Logger instance")


l1 = Logger()
l2 = Logger()
print(l1 is l2)  # True

c1 = ConfigManager()
print(c1 is a)  # True — still the same ConfigManager singleton from before

Why functools.wraps(cls, updated=[])?

Normally functools.wraps is used on functions, but here it’s applied to a class. The updated=[] argument matters: by default, wraps tries to update __dict__ on the wrapper with the wrapped object’s __dict__, which works fine for functions but can cause issues when the wrapped object is a class (since a class’s __dict__ is a mappingproxy, not something you generally want merged onto a function). Passing updated=[] skips that step while still copying over __name__, __doc__, __module__, and __qualname__, so ConfigManager.__name__ still reports "ConfigManager" correctly for debugging and introspection.

An Important Caveat: get_instance Is a Function, Not a Class

After decoration, ConfigManager is no longer actually a class — it’s the get_instance function. This has real consequences:

print(type(ConfigManager))          # <class 'function'>
print(isinstance(a, ConfigManager)) # TypeError: isinstance() arg 2 must be a type...

isinstance checks fail because ConfigManager is no longer a type at all. If your code needs isinstance checks against the singleton class, this decorator approach isn’t the right fit — you’d want a metaclass-based singleton instead (covered below), which preserves the class as an actual class.

Alternative 1: Singleton via __new__

A more “class stays a class” approach overrides __new__ directly:

class ConfigManager:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        print("Init called")
        self.settings = {}


a = ConfigManager()
b = ConfigManager()
print(a is b)  # True

One subtlety worth knowing: __init__ runs every time ConfigManager() is called, even though __new__ returns the same object — because Python always calls __init__ after __new__ returns an instance of the class, regardless of whether that instance is new or reused. In the example above, "Init called" prints twice, and self.settings = {} resets on every “construction,” which may or may not be what you want. Guarding against that requires extra bookkeeping:

class ConfigManager:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialized = False
        return cls._instance

    def __init__(self):
        if self._initialized:
            return
        print("Init called")
        self.settings = {}
        self._initialized = True

Alternative 2: Singleton via Metaclass

A metaclass-based singleton preserves isinstance checks correctly, since the resulting object is a genuine class the whole way through:

class SingletonMeta(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 ConfigManager(metaclass=SingletonMeta):
    def __init__(self):
        print("Creating ConfigManager instance")
        self.settings = {}


a = ConfigManager()
b = ConfigManager()
print(a is b)                        # True
print(isinstance(a, ConfigManager))  # True — works correctly, unlike the decorator version
print(type(ConfigManager))           # <class '__main__.SingletonMeta'>

This intercepts ConfigManager(...) at the metaclass level via __call__ — the same underlying mechanism Python uses for all class instantiation — so ConfigManager remains a proper type instance the whole time.

Choosing Between the Three Approaches

Approachisinstance worksReusable across classesComplexity
DecoratorNoYes, easilyLow
__new__ overrideYesOnly by copying the code into each class (or a mixin)Medium
MetaclassYesYes, via metaclass=SingletonMetaMedium-High

For most application code, I reach for the decorator when I just need “only one instance exists” and don’t need isinstance checks — it’s the least code and the most obviously reusable. When isinstance matters, or the class already needs a custom metaclass for other reasons, the metaclass approach is more correct.

Thread Safety

None of the versions above are thread-safe by default — two threads could both pass the if cls not in instances check simultaneously before either writes, creating two instances. For genuinely concurrent code, add a lock:

import threading
import functools

def singleton(cls):
    instances = {}
    lock = threading.Lock()

    @functools.wraps(cls, updated=[])
    def get_instance(*args, **kwargs):
        if cls not in instances:
            with lock:
                if cls not in instances:  # double-checked locking
                    instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return get_instance

The double check (once outside the lock, once inside) avoids paying the locking cost on every call after the instance already exists, while still being safe against the race during first creation.

Testing Code That Uses Singletons

Singletons are notoriously awkward in tests because state persists across test cases unless explicitly reset. A common pattern is exposing a reset hook:

def singleton(cls):
    instances = {}

    @functools.wraps(cls, updated=[])
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    def reset():
        instances.pop(cls, None)

    get_instance.reset = reset
    return get_instance


@singleton
class ConfigManager:
    def __init__(self):
        self.settings = {}


# In a test teardown:
ConfigManager.reset()

This is a small but important addition — without it, singleton state can silently leak between otherwise-independent tests, causing failures that only show up depending on test execution order.

Common Mistakes

  • Using a singleton as a substitute for proper dependency injection, hiding a hard dependency inside every function that happens to call ConfigManager(), which makes testing and reasoning about the code harder.
  • Forgetting __init__ re-runs on every “construction” with the __new__-based approach, silently resetting state you thought was preserved.
  • Not considering thread safety in multi-threaded applications, risking duplicate instances under race conditions.
  • Reaching for a singleton when a simple module-level instance would do. A plain object created once at module import time is often simpler and just as effective, since Python modules are already singletons by nature (imported once, cached in sys.modules).

FAQs

Q: Is a singleton really necessary, or could I just use a module-level variable? Often the module-level variable is simpler and equally effective — Python only imports a module once per process, so instance = ConfigManager() at module level naturally behaves like a singleton without any pattern machinery at all. Reach for the formal singleton pattern when you specifically need to control instantiation behavior of the class itself (e.g., a library class other code constructs directly).

Q: Does the decorator singleton work with subclassing? Not cleanly — since decoration replaces the class with a function, subclassing ConfigManager after decoration doesn’t behave like normal class inheritance. If you need singleton behavior and subclassing, the metaclass approach is far better suited.

Q: Are singletons considered an anti-pattern? They’re controversial rather than universally discouraged — the criticism is really about overuse as a workaround for proper dependency management, not that the pattern itself is invalid. Used deliberately for a genuine “exactly one of these should exist” requirement, it’s a legitimate, well-established pattern.

Summary

Creating a singleton with a decorator is a compact, reusable way to enforce “only one instance of this class” — a dictionary closed over by the decorator tracks one instance per decorated class, and every call returns that same object. It trades away isinstance compatibility (since the class becomes a wrapper function) in exchange for simplicity and easy reuse across multiple classes; when isinstance checks matter, a metaclass-based singleton is the more correct tool. Whichever approach you pick, add thread-safety if the code runs concurrently, and consider whether a plain module-level instance might solve the same problem with less machinery.

References

Total
0
Shares

Leave a Reply

Previous Post
Decorator with arguments in python

Decorator with Arguments in Python: Complete Advanced Function Wrapping and Customization Guide

Next Post
Using a decorator to time a function in python

Using a Decorator to Time a Function in Python: Complete Performance Measurement and Optimization Guide

Related Posts