Implement a singleton in Python
Compare module-level instances, metaclass caching, and __new__ in Python, including repeated initialization, thread safety, and testing trade-offs.
For one shared object inside a Python application, a module-level instance is often sufficient. If construction must be controlled through a class, a metaclass can cache an instance per class. Neither approach creates a single object shared across separate processes.
Cache an instance with a metaclass
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):
pass
print(Logger() is Logger()) # True
The metaclass intercepts ordinary construction. The first call constructs and initializes an object; later calls return it without calling its initializer again. Later constructor arguments are therefore ignored. This minimal example is not thread-safe: concurrent calls can race between the check and creation. It also does not define special behavior for copying, pickling, or unusual construction paths.
Cache through new
class Logger:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
This example suits a simple class without a subclassing contract. Python can still call __init__ on every normal construction, even when __new__ returns an existing instance. Guard initialization if it has side effects, or choose another design. Inheritance also needs care: a subclass can inherit the parent's cached _instance.
Use a module-level object
# app_logger.py
class Logger:
pass
logger = Logger()
Other modules can use from app_logger import logger. Ordinary imports reuse a module cached under its import name. Reloading a module, loading it under another name, or running another process can create another instance, so avoid describing this as an unconditional global guarantee.
FAQ
How do I handle threads? A lock can protect the cache lookup and creation, but think about reentrant construction and initialization dependencies as well. Creating one shared object during startup and passing it to its consumers is often simpler. Sharing the instance does not make its methods thread-safe.
What are the design costs? Shared mutable state can hide dependencies and leak between tests. Dependency injection gives tests explicit control over instance lifetime and replacements.
Can I use a decorator? Yes. A wrapper can cache the result of constructing a class. A simple function decorator replaces the class name with a function, however, which can complicate isinstance, introspection, and subclassing. It is not interchangeable with preserving a normal class interface.
References
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan