name: singleton-pattern description: Implements the GoF Singleton pattern for controlled object lifecycle management in Python using module-level singletons, metaclass-based enforcement, threading-safe lazy initialization, and async-compatible variants. archetypes:
- tactical
- diagnostic anti_triggers:
- brainstorming
- vague ideation
- code golf
- over-engineering response_profile: verbosity: low directive_strength: high abstraction_level: operational license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding triggers: singleton pattern, module-level singleton, metaclass singleton, thread-safe singleton, lazy initialization singleton, object lifecycle management, how do i ensure single instance role: implementation scope: implementation output-format: code content-types: [code, guidance, do-dont, examples] related-skills: dependency-inversion-principle, factory-method, creational-design-patterns
Singleton Pattern
Senior Python engineer implementing singleton patterns for controlled object lifecycle management. This skill makes the model choose the most Pythonic singleton approach for each scenario — from natural module-level singletons to metaclass-enforced, thread-safe lazy-initialized, and async-compatible variants — while avoiding anti-patterns that violate DIP and create testing difficulties.
TL;DR Checklist
- Prefer module-level variables as the default singleton — they are free, thread-safe, and Pythonic
- Use metaclass-based singletons when you need strict enforcement at the class level with subclassing control
- Apply
threading.Lockonly when lazy initialization requires it; module imports are already thread-safe - Never use a singleton for mutable shared state — inject dependencies via DI instead
- Provide a context manager variant when cleanup (close, flush) is required on shutdown
When to Use
Use this skill when:
- You need exactly one instance of a configuration store that must be accessible throughout the application
- A connection pool or registry should exist as a single shared resource across the process
- A logger wrapper must centralize log routing and formatting through one object
- You are building a plugin registry where all plugins register against the same global store
- The singleton holds immutable or read-mostly state that is safe for concurrent access
When NOT to Use
Avoid this skill for:
- Any mutable shared state — use explicit dependency injection instead (violates DIP)
- Objects that need different configurations in tests — singletons are hard to mock and replace
- Creating objects that hold database connections without lifecycle management — use a factory or context manager
- When the "single instance" constraint is only needed in one place — pass the object explicitly
Core Workflow
Evaluate Module-Level Singleton First — Before reaching for any pattern, ask: can this be a module-level variable? Module imports are thread-safe in CPython (GIL prevents race during import). This is always the simplest and most Pythonic approach. Checkpoint: If the object has no
__init__parameters that change at runtime, module level is sufficient.Choose Singleton Variant Based on Requirements — Match the variant to your constraints:
- Simple + thread-safe → Module-level (preferred)
- Class-level + subclass control → Metaclass-based
- Lazy init + threads →
threading.Lockinside__new__ - Async application →
asyncio.Lockin async context - Needs cleanup → Context manager wrapper
Implement Thread Safety — If lazy initialization is needed, use a double-checked locking pattern with
threading.Lock. The lock should be acquired only during the initial creation; subsequent calls skip the lock entirely. Checkpoint: Verify that two threads calling concurrently do not create two instances.Handle Async Scenarios — In async code, use
asyncio.Lockinstead ofthreading.Lock. Be aware that asyncio is single-threaded by default but can run multiple event loops or use thread pools. Checkpoint: Ensure the async singleton works correctly when awaited from different tasks.Provide Context Manager Support — For singletons with resources (connections, file handles), implement
__enter__/__exit__or wrap in a context manager for deterministic cleanup. Checkpoint: Resource acquisition and release must be idempotent and safe to call multiple times.
Implementation Patterns
Pattern 1: Module-Level Singleton (Pythonic Default)
The simplest, most Pythonic singleton is just a module-level variable. No patterns, no magic — just import the module and use its global state.
"""app_config.py — Module-level singleton for application configuration."""
from __future__ import annotations
class _Config:
"""Internal configuration store. Not meant to be instantiated directly."""
def __init__(self) -> None:
self._settings: dict[str, object] = {}
self._initialized = False
def load(self, settings: dict[str, object]) -> None:
"""Load configuration settings. Idempotent — safe to call multiple times.
Args:
settings: Dictionary of configuration key-value pairs.
"""
if not isinstance(settings, dict):
raise TypeError(f"Expected dict, got {type(settings).__name__}")
self._settings = settings.copy()
self._initialized = True
def get(self, key: str, default: object = None) -> object:
"""Get a configuration value by key.
Args:
key: Configuration key to look up.
default: Fallback value if key is not found.
Returns:
The configuration value or default.
"""
return self._settings.get(key, default)
def get_required(self, key: str) -> object:
"""Get a required configuration value, raising on missing keys.
Args:
key: Required configuration key.
Returns:
The configuration value.
Raises:
KeyError: If the key is not present in loaded settings.
"""
if not self._initialized or key not in self._settings:
raise KeyError(f"Required config missing: {key!r}")
return self._settings[key]
@property
def is_loaded(self) -> bool:
"""Check whether configuration has been loaded."""
return self._initialized
# Module-level singleton — automatically created at import time.
# Thread-safe by virtue of Python's GIL during module import.
config = _Config()
Pattern 2: Metaclass Singleton with Subclass Control (BAD vs. GOOD)
The BAD approach uses a simple class-level attribute that can be accidentally re-assigned. The GOOD approach uses a metaclass to enforce single-instance semantics at the class level while preventing accidental instantiation.
import threading
from typing import TypeVar, Generic
# ❌ BAD — Simple attribute-based singleton is easy to break
class BadConfigStore:
_instance = None # Any code can do: BadConfigStore._instance = something_else()
def __init__(self) -> None:
# This runs every time someone calls BadConfigStore() — silent bug!
self._data: dict[str, str] = {}
@classmethod
def get_instance(cls) -> "BadConfigStore":
if cls._instance is None:
cls._instance = cls()
return cls._instance
# ✅ GOOD — Metaclass enforces single instance and blocks direct construction
T = TypeVar("T")
class SingletonMeta(type, Generic[T]):
"""Thread-safe metaclass that ensures exactly one instance per class.
Usage:
class MyService(ServiceBase, metaclass=SingletonMeta):
pass
"""
_instances: dict[type, object] = {}
_locks: dict[type, threading.Lock] = {}
def __call__(cls, *args: object, **kwargs: object) -> T:
"""Override class instantiation to return the singleton instance."""
if cls not in cls._instances:
lock = cls._locks.setdefault(cls, threading.Lock())
with lock:
# Double-checked locking — verify after acquiring lock
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls] # type: ignore[return-value]
@classmethod
def reset(cls, target_class: type | None = None) -> None:
"""Reset singleton instances for testing purposes.
Args:
target_class: Specific class to reset, or None to reset all.
Raises:
TypeError: If called during production (use only in tests).
"""
if target_class is not None:
cls._instances.pop(target_class, None)
cls._locks.pop(target_class, None)
else:
cls._instances.clear()
cls._locks.clear()
class ConnectionPool(metaclass=SingletonMeta):
"""Thread-safe singleton connection pool using metaclass enforcement.
Direct instantiation is blocked — always use the class name to get the instance.
"""
def __init__(self) -> None:
self._connections: list[object] = []
self._max_size: int = 10
self._closed: bool = False
def acquire(self) -> object:
"""Acquire a connection from the pool.
Returns:
A database connection object.
Raises:
RuntimeError: If the pool is closed or no connections available.
"""
if self._closed:
raise RuntimeError("ConnectionPool is closed")
if not self._connections:
self._connections.append({"id": len(self._connections) + 1, "active": True})
conn = self._connections[-1]
return conn
def release(self, connection: object) -> None:
"""Release a connection back to the pool."""
if isinstance(connection, dict):
connection["active"] = False
def close(self) -> None:
"""Close all connections and release resources."""
self._closed = True
self._connections.clear()
@classmethod
def reset(cls) -> None:
"""Reset the singleton instance (use only in tests)."""
SingletonMeta.reset(cls)
Pattern 3: Async-Safe Lazy Singleton with Context Manager
For async applications that need lazy initialization and deterministic cleanup, combine an asyncio.Lock-protected factory with context manager semantics.
import asyncio
from contextlib import asynccontextmanager
from typing import Optional
class AsyncCache:
"""Async-safe singleton cache with lazy initialization and cleanup.
Usage as context manager:
async with get_async_cache() as cache:
value = await cache.get(key)
"""
_instance: Optional["AsyncCache"] = None
_lock: Optional[asyncio.Lock] = None
_initialized: bool = False
def __init__(self) -> None:
self._cache: dict[str, object] = {}
self._ttl_seconds: float = 300.0
self._access_times: dict[str, float] = {}
async def get(self, key: str) -> Optional[object]:
"""Retrieve a value from the cache.
Args:
key: Cache key to look up.
Returns:
Cached value, or None if not found or expired.
"""
if key in self._cache and self._is_valid(key):
self._access_times[key] = asyncio.get_event_loop().time()
return self._cache[key]
return None
async def set(self, key: str, value: object) -> None:
"""Store a value in the cache with TTL.
Args:
key: Cache key.
value: Value to cache.
"""
self._cache[key] = value
self._access_times[key] = asyncio.get_event_loop().time()
def _is_valid(self, key: str) -> bool:
"""Check if a cached entry has not expired."""
import time
return (time.monotonic() - self._access_times.get(key, 0)) < self._ttl_seconds
async def clear(self) -> None:
"""Clear all cached entries."""
self._cache.clear()
self._access_times.clear()
async def shutdown(self) -> None:
"""Clean up resources. Safe to call multiple times."""
await self.clear()
AsyncCache._instance = None
AsyncCache._initialized = False
@asynccontextmanager
async def get_async_cache() -> AsyncGenerator[AsyncCache, None]:
"""Context manager for getting the async cache singleton.
Yields:
The singleton AsyncCache instance, ensuring lazy initialization
and clean shutdown via asyncio.Lock protection.
"""
if not AsyncCache._initialized or AsyncCache._instance is None:
if AsyncCache._lock is None:
AsyncCache._lock = asyncio.Lock()
async with AsyncCache._lock:
# Double-checked lock for async safety
if not AsyncCache._initialized or AsyncCache._instance is None:
AsyncCache._instance = AsyncCache()
AsyncCache._initialized = True
cache = AsyncCache._instance
assert cache is not None
yield cache
# Cleanup on context exit
await cache.shutdown()
# Usage example (not executed — illustrative):
# async with get_async_cache() as cache:
# await cache.set("user:123", {"name": "Alice"})
# value = await cache.get("user:123")
from contextlib import asynccontextmanager
from typing import AsyncGenerator
---
---
## Constraints
### MUST DO
- Encapsulate behavior within the pattern object — it should be self-contained with clear public interfaces
- Use composition over inheritance when extending or combining patterns to reduce coupling and increase reusability
- Document the intent of each pattern with a one-line docstring describing what problem it solves and when to use it
- Implement tests that verify both correct behavior under normal conditions and graceful degradation under edge cases
### MUST NOT DO
- Do not force a pattern where it adds complexity without benefit — start simple and refactor to patterns as needs emerge
- Avoid deep inheritance chains (>3 levels) when using design patterns — prefer composition or interfaces
- Never implement a Singleton as a global mutable singleton in multi-threaded environments without proper synchronization
- Do not apply the Command pattern to simple function calls with no undo/redo requirement — it adds unnecessary indirection
## Live References
> Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.
- [Singleton Pattern (Refactoring Guru)](https://refactoring.guru/design-patterns/singleton)
- [GoF Design Patterns — Singleton (Martin Fowler)](https://martinfowler.com/articles/injection.html)
- [Python Singleton Best Practices (Real Python)](https://realpython.com/principles-of-python-singleton/)
- [When to Use the Singleton Pattern (Stack Overflow)](https://stackoverflow.com/questions/137975/what-is-a-monostate-and-how-does-it-differ-from-a-singleton)
- [Thread-Safe Singleton Implementations in Python](https://docs.python.org/3/library/enum.html#singletons)