Elegant Singleton Implementation in Python: Module-based and Decorator Approaches

Nov 19, 2025 · Programming · 13 views · 7.8

Keywords: Python | Singleton Pattern | Design Patterns | Modularization | Decorators

Abstract: This article provides an in-depth exploration of various singleton pattern implementations in Python, focusing on the natural advantages of using modules as singletons. It also covers alternative approaches including decorators, __new__ method, metaclasses, and Borg pattern, with practical examples and comparative analysis to guide developers in making informed implementation choices.

Core Concepts of Singleton Pattern and Python Characteristics

The singleton pattern, as a classic design pattern, aims to ensure that a class has only one instance and provides a global point of access. In Python, a dynamic language lacking private constructors, implementing singletons requires careful consideration of language features.

Modules as Natural Singletons

Python modules are cached upon import, making them naturally suitable for singleton patterns. By encapsulating related functionality and state in module-level variables and functions, the complexity of class instantiation can be avoided.

# config.py
DATABASE_CONFIG = {
    'host': 'localhost',
    'port': 5432,
    'user': 'admin'
}

def get_database_connection():
    # Return database connection
    pass

When imported in other modules, the same configuration object is always accessed:

# app.py
from config import DATABASE_CONFIG, get_database_connection

# All imports share the same DATABASE_CONFIG
print(DATABASE_CONFIG['host'])  # Output: localhost

Decorator-based Singleton Implementation

When classes are necessary, decorators provide a clean approach to singleton implementation by wrapping the target class and controlling the instantiation process.

class SingletonDecorator:
    """
    Singleton decorator implementation ensuring the decorated class has only one instance
    """
    def __init__(self, cls):
        self._cls = cls
        self._instance = None
    
    def instance(self):
        """Get singleton instance"""
        if self._instance is None:
            self._instance = self._cls()
        return self._instance
    
    def __call__(self):
        raise TypeError('Please use instance() method to get singleton instance')

@SingletonDecorator
class DatabaseManager:
    def __init__(self):
        self.connections = {}
        print('DatabaseManager instance created')

# Usage
db1 = DatabaseManager.instance()
db2 = DatabaseManager.instance()
print(db1 is db2)  # Output: True

Singleton via __new__ Method

By overriding the class's __new__ method, the instantiation process can be controlled at the object creation stage, providing another common singleton implementation.

class SingletonClass:
    _instance = None
    
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def __init__(self):
        # Initialization code
        self.initialized = True

s1 = SingletonClass()
s2 = SingletonClass()
print(s1 is s2)  # Output: True

Metaclass-based Singleton Pattern

Metaclasses provide control mechanisms at the class creation stage, enabling more complex singleton logic.

class SingletonMeta(type):
    """Singleton metaclass"""
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Configuration(metaclass=SingletonMeta):
    def __init__(self):
        self.settings = {}
        print('Configuration instance created')

config1 = Configuration()
config2 = Configuration()
print(config1 is config2)  # Output: True

Borg Pattern: Shared State Instead of Identity

The Borg pattern (or Monostate pattern) achieves singleton-like behavior by sharing instance state rather than enforcing identical identity.

class Borg:
    _shared_state = {}
    
    def __init__(self):
        self.__dict__ = self._shared_state

class AppConfig(Borg):
    def __init__(self):
        super().__init__()
        if not hasattr(self, 'initialized'):
            self.database_url = 'postgresql://localhost:5432/mydb'
            self.api_key = 'secret_key'
            self.initialized = True

config1 = AppConfig()
config2 = AppConfig()
config1.api_key = 'new_key'
print(config2.api_key)  # Output: new_key
print(config1 is config2)  # Output: False, but state is shared

Practical Application: Pydantic Configuration Management

In real-world projects, configuration management is a typical use case for singletons. Combining Python's functools.lru_cache provides an elegant way to implement configuration singletons.

from functools import lru_cache
from pydantic import BaseSettings

class AppSettings(BaseSettings):
    """Application configuration class"""
    database_url: str = 'sqlite:///./app.db'
    secret_key: str = 'default_secret'
    debug: bool = False

@lru_cache(maxsize=1)
def get_settings() -> AppSettings:
    """Get singleton configuration instance"""
    return AppSettings()

# Usage in different modules
# module_a.py
from config import get_settings
settings = get_settings()

# module_b.py  
from config import get_settings
settings = get_settings()  # Returns the same instance

Comparative Analysis of Implementation Approaches

Module Singleton: Simplest and most direct, suitable for functional singleton needs but lacks class flexibility.

Decorator Singleton: Clear code structure, explicit usage, but requires calling specific methods to get instances.

__new__ Method: Follows conventional instantiation syntax but may encounter issues in certain inheritance scenarios.

Metaclass Singleton: Powerful functionality supporting complex control logic but has higher learning and usage barriers.

Borg Pattern: Allows different instances while sharing state, suitable for scenarios requiring multiple "instances" with shared data.

Best Practice Recommendations

Prioritize using modules as singletons, especially for configuration, utility functions, and similar scenarios.

When classes are necessary, choose the appropriate implementation based on project complexity.

Consider thread safety; additional synchronization mechanisms are needed in multi-threaded environments.

In framework development, provide built-in singleton support, such as Pydantic's configuration management.

By appropriately selecting singleton implementation approaches, code simplicity can be maintained while meeting specific project requirements.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.