Software Engineer's Blog

Python Singleton Implementation Guide (Pythonic & Practical)

Python Singleton Implementation Guide (Pythonic & Practical)

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.


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
  • 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

ApproachComplexityTestabilityRecommended Usage
Module-levelVery lowExcellent✅ Most backend & web apps
MetaclassMediumModerateLibraries / frameworks
__new__ overrideLowPoorLegacy 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