
Are You Making These Critical Mistakes with Python Singletons?
Singletons are a classic design pattern used to ensure a class has only one instance and provides a global point of access to it. In Python, there are several ways to implement a singleton, each with its own trade-offs. This post walks through the most common approaches, their pitfalls, and best practices for robust singleton design.
1. Why Use a Singleton?#
Sometimes you need a single, shared resource—like a configuration manager, logger, or database connection. The singleton pattern ensures only one instance of such a class exists, and that all code uses the same instance.
Here are some practical examples where software engineers use the Singleton pattern in Python:
- Database Connection Manager
- Configuration Manager
- Logger
- Cache Manager
You initialise your logger object with the same configurations and use it across your application. The same goes with your configuration manager or you config, where you might want to initialise and get all the secrets once, without having to call your secret manager every time you need to use your conf.
1.1 Singleton pattern's controversial status#
There are some developers that try to avoid this pattern because it Introduces hidden dependencies and global state and usually its easily overused or misused. Additionally, it's hard to test these objects.
Nevertheless, let's explore some examples in Python, and you can make your opinion at the end of this article.
💡 Looking for the code? You can find all the code examples from this article in GitHub.
2. The Classic __new__
Singleton#
The most basic way to implement a singleton in Python is to override the __new__
method:
class ConfigManager:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
print("ConfigManager __init__ called")
How it works:
__new__
ensures only one instance is created.- Every call to
ConfigManager()
returns the same object.
2.1. The __init__
Pitfall#
Limitation: Even though __new__
returns the same instance, __init__
is called every time you instantiate the class. This can lead to bugs if your constructor modifies state or performs side effects.
Example:
c1 = ConfigManager()
c2 = ConfigManager()
# Both are the same instance, but __init__ runs twice!
3. Singleton with a Decorator#
3.1 What is a Decorator and Why Use It?#
A decorator in Python is a function that takes another function or class and returns a modified version of it. Decorators are a powerful way to add reusable behavior to classes or functions without modifying their code directly.
For singletons, a decorator can wrap a class so that only one instance is ever created, regardless of how many times you instantiate it.
A decorator can be used to enforce the singleton pattern:
def singleton_decorator(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton_decorator
class ConfigManager:
def __init__(self):
print("ConfigManager __init__ called")
How it works:
- The decorator wraps the class, ensuring only one instance is created.
__init__
is only called once.
3.2 Decorator Limitations#
While the decorator approach is simple, it comes with two major issues:
3.2.1 Inheritance Issue#
When you decorate a class with a singleton decorator, the class becomes a function, not a type. This means:
- You cannot subclass the decorated class (subclassing raises a
TypeError
). isinstance
andissubclass
checks do not work as expected.
Example:
@singleton_decorator
class Logger:
def __init__(self, name="Logger"):
self.name = name
print(f"Created {self.name}")
print(f"Logger type: {type(Logger)}") # <class 'function'> - Problem!
# This will fail because Logger is now a function, not a class
try:
class FileLogger(Logger): # Can't inherit from a function!
pass
except TypeError as e:
print(f"Error: {e}")
# TypeError: function() argument 'code' must be code, not str
3.2.2 Not Thread-Safe#
In multi-threaded code, two threads can create two instances if they race to check for the instance at the same time. The decorator is not thread-safe by default.
Example:
import threading
import time
from concurrent.futures import ThreadPoolExecutor
def unsafe_singleton(cls):
"""Decorator singleton - NOT thread safe"""
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
# PROBLEM: Race condition here!
# Multiple threads can pass this check simultaneously
time.sleep(0.01) # Simulate slow initialization
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
def demonstrate_unsafe_decorator():
"""Shows race condition with unsafe decorator singleton"""
print("UNSAFE DECORATOR - RACE CONDITION")
print("=" * 40)
@unsafe_singleton
class Database:
def __init__(self):
self.id = id(self)
print(f"Database created with ID: {self.id}")
def create_database():
return Database()
# Run multiple threads simultaneously
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(create_database) for _ in range(5)]
instances = [future.result() for future in futures]
# Check if all instances are the same
unique_ids = set(inst.id for inst in instances)
print(f"Number of unique instances created: {len(unique_ids)}")
print(f"Should be 1, but got: {len(unique_ids)} (RACE CONDITION!)")
if len(unique_ids) > 1:
print("❌ FAILED: Multiple instances created!")
else:
print("✅ PASSED: Only one instance created")
When we run the above snippet, we can find the following:
UNSAFE DECORATOR - RACE CONDITION
========================================
Database created with ID: 4314560800Database created with ID: 4314561232
Database created with ID: 4314560128
Database created with ID: 4314559552
Database created with ID: 4314558352
Number of unique instances created: 5
Should be 1, but got: 5 (RACE CONDITION!)
❌ FAILED: Multiple instances created!
How to fix: Use a lock in the decorator or use a metaclass-based singleton.
def safe_singleton(cls):
"""Decorator singleton - Thread safe with lock"""
instances = {}
lock = threading.Lock()
def get_instance(*args, **kwargs):
if cls not in instances:
with lock: # Thread-safe with lock
if cls not in instances: # Double-check pattern
time.sleep(0.01) # Simulate slow initialization
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
SAFE DECORATOR - WITH LOCK
========================================
Database created with ID: 4314559456
Number of unique instances created: 1
✅ PASSED: Only one instance created
4. Singleton with a Metaclass#
A metaclass can enforce the singleton pattern while preserving class identity and supporting inheritance:
import os
from random import randint
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(f"ConfigManager initializing with random number: {randint(1, 100)}")
self._load_config()
def _load_config(self):
self._config = {
"api_key": os.getenv("API_KEY"),
"debug_mode": os.getenv("DEBUG", "False").lower() == "true",
"max_connections": int(os.getenv("MAX_CONN", "10")),
}
def get(self, key, default=None):
return self._config.get(key, default)
class DatabaseConfigManager(ConfigManager):
def __init__(self):
print(
f"DatabaseConfigManager initializing with random number: {randint(1, 100)}"
)
super().__init__()
self._load_db_config()
def _load_db_config(self):
# Add database-specific configuration
self._config.update(
{
"db_host": os.getenv("DB_HOST", "localhost"),
"db_port": int(os.getenv("DB_PORT", "5432")),
"db_name": os.getenv("DB_NAME", "myapp"),
}
)
def get_db_url(self):
return f"postgresql://{self.get('db_host')}:{self.get('db_port')}/{self.get('db_name')}"
if __name__ == "__main__":
# Usage: Configuration loaded once, shared everywhere
c1 = ConfigManager()
c2 = ConfigManager()
print(c1 == c2) # Should be True, both are the same instance
print(c1 is c2) # Should also be True, both variables point to the same instance
# Test DatabaseConfigManager singleton
db1 = DatabaseConfigManager()
db2 = DatabaseConfigManager()
print(db1 == db2) # Should be True
print(db1 is db2) # Should be True
print(db1.get_db_url()) # Test functionality
When we run the above script, we can see the following:
ConfigManager
: The constructor was initialised only once. We can identify that because we only see one print statement:ConfigManager initializing with random number: 54
DatabaseConfigManager
: Same thing happens to the subclass, but since we call thesuper().__init__()
, since we also call the constructor from the parent class- The metaclass controls instance creation, ensuring only one instance per class.
- Solves all 2 issues:
- Only one instance is created and
__init__
is called once. - Inheritance works as expected (
isinstance
,issubclass
, subclassing).
- Only one instance is created and
4.1 Thread-Safety#
Unfortunately, this method is not thread safe out of the box, but we can use the same solution as the decorator:
class ThreadSafeSingletonMeta(type):
"""Metaclass singleton - Naturally easier to make thread safe"""
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
time.sleep(0.01) # Simulate slow initialization
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
METACLASS - THREAD SAFE
========================================
Database created with ID: 4301632000
Number of unique instances created: 1
✅ PASSED: Only one instance created
5. Summary Table and Recommendations#

Recommendations:
- Use a decorator for simple, single-class singletons or when team familiarity with metaclasses is low.
- Use a metaclass for complex applications, inheritance, or when you need thread safety.
6. Conclusion#
The "Gang of Four" book—Design Patterns: Elements of Reusable Object-Oriented Software (1994)—is a foundational text in software engineering written by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. This influential work introduced 23 classic design patterns that became the cornerstone of object-oriented software development. Listen to this podcast if you want to find out more.
Interestingly, Erich Gamma himself has expressed reservations about the Singleton pattern, stating: "I'm in favor of dropping Singleton. Its use is almost always a design smell." He further elaborated that "it's often a design shortcut... you have a well-known place to get to another object rather than reaching to other objects" and warned that "it's very easy to add global state, but it's very hard to take it out."
Despite this criticism from one of the pattern's original authors, I believe the Singleton pattern still has its place in Python development. Unlike languages such as C++ where global state management is more problematic, Python's design and ecosystem make certain singleton use cases both practical and maintainable, particularly for configuration managers, loggers, and connection pools.
- Use the
__new__
approach for simple cases, but beware of the__init__
pitfall - Decorators are simple but break inheritance and aren't thread-safe by default
- Metaclasses provide the most robust and flexible solution for singletons in Python
Choose the approach that best fits your needs, and be aware of the trade-offs!