Python Singleton Implementation Guide (Pythonic & Practical)
-
Jason Yang - 08 Jan, 2026
- Updated 08 Jan, 2026
- Views —
The Singleton pattern ensures that only one instance of a class exists throughout the lifetime of an application. In Python, however, the way you implement a singleton differs significantly from traditional OOP-heavy languages like Java or C++.
This guide explains Pythonic, practical approaches to singletons, when to use them, and when not to.
⚠️ Important mindset
In Python, singletons are needed far less often than many developers assume.
In many cases, dependency injection (DI), application context objects, or modules themselves are better and more testable alternatives.
1. Module-Level Singleton (Recommended)
Python modules are executed once per process and cached by the import system. This makes module-level singletons the most natural and Pythonic solution.
Example: Lazy Initialization
# infrastructure/clients.py
from typing import Optional
class HeavyClient:
def __init__(self):
print("🚀 Initializing heavy client...")
self.connected = True
_client_instance: Optional[HeavyClient] = None
def get_heavy_client() -> HeavyClient:
global _client_instance
if _client_instance is None:
_client_instance = HeavyClient()
return _client_instance
Thread Safety — The Accurate Explanation
- ✅ Module imports are thread-safe (protected by Python’s import lock)
- ⚠️ Lazy-loading functions are not inherently thread-safe
In highly concurrent environments, two threads could theoretically create the instance at the same time.
Thread-safe variant (only if needed)
import threading
_lock = threading.Lock()
def get_heavy_client() -> HeavyClient:
global _client_instance
with _lock:
if _client_instance is None:
_client_instance = HeavyClient()
return _client_instance
Why this is recommended
- Extremely simple
- Easy to mock and reset in tests
- Works perfectly with FastAPI
Depends() - Aligns with real-world Python practices
2. Singleton via Metaclass
Using a metaclass allows you to inject singleton behavior at class-creation time. This approach is mainly useful for libraries or frameworks, not everyday application code.
Thread-safe implementation
from threading import Lock
class SingletonMeta(type):
_instances = {}
_lock = Lock()
def __call__(cls, *args, **kwargs):
with cls._lock:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class DatabaseConnector(metaclass=SingletonMeta):
def __init__(self):
print("Creating database connection")
# Usage
db1 = DatabaseConnector()
db2 = DatabaseConnector()
print(db1 is db2) # True
Pros
- Singleton guaranteed on every instantiation call
- Reusable across multiple classes
- Useful for framework-level abstractions
Cons
- Higher complexity
- Harder to reset or mock in tests
- Often unnecessary in application code
3. Overriding __new__
This is the traditional OOP-style singleton, controlling instance creation directly via __new__.
Example with __init__ guard
class SimpleSingleton:
_instance = None
_initialized = False
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if self._initialized:
return
print("Initializing once")
self._initialized = True
Caveats
__init__may run multiple times without a guard- Easy to introduce subtle bugs
- Poor testability and maintainability
👉 Best reserved for legacy code compatibility
Comparison Summary
| Approach | Complexity | Testability | Recommended Usage |
|---|---|---|---|
| Module-level | Very low | Excellent | ✅ Most backend & web apps |
| Metaclass | Medium | Moderate | Libraries / frameworks |
__new__ override | Low | Poor | Legacy systems only |
Practical Best Practices
(1) Prefer Lazy Initialization
Create the singleton only when it is actually needed, not at application startup.
(2) Add Locks Only When Necessary
- Most FastAPI / Uvicorn setups are fine without locks
- Use locking only when real contention is expected
(3) Design for Testing First
Singletons complicate testing. Provide a reset hook when needed.
def reset_heavy_client():
global _client_instance
_client_instance = None
Final Takeaway
In Python, singletons are a choice, not a default.
When in doubt, prefer:Module-level objectsDependency injectionExplicit application context
Default choice: Module-level singleton
Advanced tools: Metaclass or __new__ — only with a clear reason