Singleton Pattern in Python: Module-Level and Class-Based
The Singleton pattern ensures a class has only one instance and provides a global point of access to it. You'll encounter this pattern when managing shared resources: configuration objects, logging...
Key Insights
- Python modules are natural singletons—imported once and cached—making module-level state the simplest and most Pythonic approach for most use cases.
- Class-based singletons using
__new__, decorators, or metaclasses provide more control but require careful thread-safety considerations in concurrent applications. - Singletons complicate testing; design with dependency injection in mind to maintain testability regardless of which implementation you choose.
Introduction to the Singleton Pattern
The Singleton pattern ensures a class has only one instance and provides a global point of access to it. You’ll encounter this pattern when managing shared resources: configuration objects, logging handlers, database connection pools, or hardware interface controllers.
Python offers unique approaches to implementing singletons that differ significantly from languages like Java or C++. The language’s module system, first-class functions, and metaclass capabilities give you multiple tools—each with distinct trade-offs.
I’ll walk through four implementations, from the simplest Pythonic approach to the most sophisticated metaclass solution. By the end, you’ll know which to reach for in different scenarios.
Module-Level Singleton (The Pythonic Way)
Python’s import system provides singleton behavior out of the box. When you import a module, Python executes it once and caches the resulting module object in sys.modules. Subsequent imports return the cached version.
This makes module-level state a natural singleton:
# config.py
import os
from dataclasses import dataclass
from typing import Optional
@dataclass
class Settings:
database_url: str
debug: bool
api_key: str
max_connections: int
_settings: Optional[Settings] = None
def get_settings() -> Settings:
global _settings
if _settings is None:
_settings = Settings(
database_url=os.getenv("DATABASE_URL", "sqlite:///default.db"),
debug=os.getenv("DEBUG", "false").lower() == "true",
api_key=os.getenv("API_KEY", ""),
max_connections=int(os.getenv("MAX_CONNECTIONS", "10")),
)
return _settings
def reset_settings() -> None:
"""For testing purposes only."""
global _settings
_settings = None
Usage is straightforward:
from config import get_settings
settings = get_settings()
print(settings.database_url)
This approach works well when you need simple shared state without class instantiation semantics. It’s explicit, easy to understand, and requires no magic. Use it for configuration, feature flags, or simple caches.
The limitation: you can’t use inheritance or polymorphism. If you need those, move to a class-based approach.
Class-Based Singleton Using __new__
Override __new__ to intercept instance creation and return an existing instance instead of creating a new one:
# database.py
import threading
from typing import Optional
class DatabaseConnection:
_instance: Optional["DatabaseConnection"] = None
_lock: threading.Lock = threading.Lock()
def __new__(cls, connection_string: str = "") -> "DatabaseConnection":
if cls._instance is None:
with cls._lock:
# Double-check locking pattern
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, connection_string: str = "") -> None:
if self._initialized:
return
self._initialized = True
self.connection_string = connection_string
self._connection = None
print(f"Initializing connection to {connection_string}")
def connect(self) -> None:
if self._connection is None:
# Simulate connection
self._connection = f"Connection<{self.connection_string}>"
print(f"Connected: {self._connection}")
def execute(self, query: str) -> str:
if self._connection is None:
raise RuntimeError("Not connected")
return f"Executed: {query}"
@classmethod
def reset_instance(cls) -> None:
"""For testing purposes only."""
with cls._lock:
cls._instance = None
Notice the double-checked locking pattern. The first check avoids acquiring the lock on every call. The second check inside the lock prevents race conditions when multiple threads pass the first check simultaneously.
The _initialized flag in __init__ prevents re-initialization. Remember: __new__ returns the instance, but __init__ still runs on every call to DatabaseConnection().
# Both return the same instance
db1 = DatabaseConnection("postgresql://localhost/mydb")
db2 = DatabaseConnection("mysql://localhost/other") # connection_string ignored
assert db1 is db2
print(db1.connection_string) # postgresql://localhost/mydb
This approach works when you need class semantics but want to ensure single instantiation. The downside: it’s not immediately obvious to callers that they’re getting a singleton.
Decorator-Based Singleton
A decorator provides reusable singleton behavior without modifying class internals:
# singleton_decorator.py
import threading
from functools import wraps
from typing import Type, TypeVar, Dict, Any
T = TypeVar("T")
def singleton(cls: Type[T]) -> Type[T]:
"""Thread-safe singleton decorator."""
instances: Dict[Type, Any] = {}
lock = threading.Lock()
@wraps(cls, updated=[])
class SingletonWrapper(cls):
def __new__(wrapper_cls, *args: Any, **kwargs: Any) -> T:
if cls not in instances:
with lock:
if cls not in instances:
instance = super().__new__(wrapper_cls)
instances[cls] = instance
return instances[cls]
@classmethod
def _reset_singleton(wrapper_cls) -> None:
"""For testing purposes only."""
with lock:
instances.pop(cls, None)
return SingletonWrapper
Apply it to any class:
@singleton
class Logger:
def __init__(self, name: str = "default") -> None:
self.name = name
self.handlers: list = []
self._initialized = getattr(self, "_initialized", False)
if not self._initialized:
print(f"Logger '{name}' initialized")
self._initialized = True
def log(self, message: str) -> None:
print(f"[{self.name}] {message}")
@singleton
class Cache:
def __init__(self) -> None:
if not hasattr(self, "_data"):
self._data: dict = {}
def get(self, key: str) -> Any:
return self._data.get(key)
def set(self, key: str, value: Any) -> None:
self._data[key] = value
The decorator approach separates singleton logic from business logic. You can apply it to existing classes without modification. However, it adds a wrapper layer that might confuse IDE type checkers and makes the inheritance chain less transparent.
Metaclass Singleton Implementation
Metaclasses control class creation itself, providing the most transparent singleton implementation:
# singleton_meta.py
import threading
from typing import Dict, Any, Type
class SingletonMeta(type):
"""Thread-safe singleton metaclass."""
_instances: Dict[Type, Any] = {}
_lock: threading.Lock = threading.Lock()
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
def reset_instance(cls) -> None:
"""For testing purposes only."""
with cls._lock:
cls._instances.pop(cls, None)
Usage is clean:
class AppConfig(metaclass=SingletonMeta):
def __init__(self, environment: str = "development") -> None:
if hasattr(self, "_initialized"):
return
self._initialized = True
self.environment = environment
self.settings: dict = {}
self._load_settings()
def _load_settings(self) -> None:
# Load environment-specific settings
self.settings = {
"development": {"debug": True, "log_level": "DEBUG"},
"production": {"debug": False, "log_level": "WARNING"},
}.get(self.environment, {})
def get(self, key: str, default: Any = None) -> Any:
return self.settings.get(key, default)
Metaclasses are ideal for frameworks or libraries where you want singleton behavior to be completely transparent to users. The class looks and behaves normally—the singleton magic happens at the metaclass level.
The trade-off: metaclasses are Python’s most advanced feature. They can conflict with other metaclasses, and many developers find them hard to reason about.
Comparison and Trade-offs
| Approach | Thread-Safe | Inheritance | Testability | Complexity | Best For |
|---|---|---|---|---|---|
| Module-level | Yes* | No | Good | Low | Config, simple state |
__new__ override |
With locking | Limited | Moderate | Medium | Single class needs |
| Decorator | With locking | Yes | Good | Medium | Reusable pattern |
| Metaclass | With locking | Yes | Moderate | High | Frameworks, libraries |
*Module imports are thread-safe in Python 3.
Testing singletons requires resetting state between tests:
# test_config.py
import pytest
from config import get_settings, reset_settings
from database import DatabaseConnection
class TestConfiguration:
def setup_method(self) -> None:
reset_settings()
DatabaseConnection.reset_instance()
def test_settings_loads_from_environment(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("DATABASE_URL", "postgresql://test/db")
monkeypatch.setenv("DEBUG", "true")
settings = get_settings()
assert settings.database_url == "postgresql://test/db"
assert settings.debug is True
def test_database_singleton_returns_same_instance(self) -> None:
db1 = DatabaseConnection("conn1")
db2 = DatabaseConnection("conn2")
assert db1 is db2
assert db1.connection_string == "conn1"
For better testability, consider dependency injection:
class Service:
def __init__(self, config: Settings | None = None) -> None:
self.config = config or get_settings()
This allows injecting mock configurations in tests while defaulting to the singleton in production.
Recommendations and Best Practices
Start with module-level state. It’s the simplest, most Pythonic approach. Only move to class-based singletons when you need inheritance, polymorphism, or explicit instantiation semantics.
Choose your implementation based on context:
- Application code with simple needs → module-level
- Single class requiring singleton behavior →
__new__override - Multiple classes needing singleton behavior → decorator
- Framework or library code → metaclass
Always include a reset mechanism for testing, but mark it clearly as test-only.
Consider alternatives. Dependency injection containers (like dependency-injector or FastAPI’s Depends) often provide better solutions than manual singletons. They give you singleton-like behavior with explicit wiring and easier testing.
Avoid these pitfalls:
- Don’t use singletons for mutable shared state that multiple threads modify—use proper synchronization or thread-local storage
- Don’t forget that
__init__runs on every “instantiation” even with__new__override - Don’t mix metaclasses carelessly—Python only allows one metaclass per class hierarchy
Singletons are a tool, not a goal. Use them when you genuinely need exactly one instance. When in doubt, start without the pattern and refactor toward it only when the need becomes clear.