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.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.