Singleton Pattern

Ensure a class has only one instance and provide a global point of access to it.


Problem

Some classes should have only one instance (logger, database connection, configuration). You need to prevent multiple instances and provide global access.

Solution

The Singleton pattern restricts instantiation to a single object and provides a global access point.


Implementation

import threading
from typing import Optional

# ============ BASIC SINGLETON ============

class Logger:
    __instance: Optional['Logger'] = None

    @staticmethod
    def get_instance() -> 'Logger':
        if Logger.__instance is None:
            Logger.__instance = Logger()
        return Logger.__instance

    def log(self, message: str):
        print(f"[LOG] {message}")

# Usage
logger1 = Logger.get_instance()
logger2 = Logger.get_instance()
print(logger1 is logger2)  # True

# ============ THREAD-SAFE SINGLETON (using __new__) ============

class DatabaseConnection:
    __instance: Optional['DatabaseConnection'] = None
    __lock = threading.Lock()

    def __new__(cls):
        """Enforce singleton - only one instance can exist"""
        if cls.__instance is None:
            with cls.__lock:
                if cls.__instance is None:
                    cls.__instance = super().__new__(cls)
                    cls.__instance._initialized = False
        return cls.__instance

    def __init__(self):
        """Only initialize once"""
        if self._initialized:
            return
        self._initialized = True
        self.connection_string = "localhost:5432"
        print("Database connection initialized")

    def query(self, sql: str):
        print(f"Executing: {sql}")

# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)  # True

# ============ METACLASS SINGLETON ============

class SingletonMeta(type):
    """Metaclass that enforces singleton pattern"""
    _instances = {}
    _lock = threading.Lock()

    def __call__(cls, *args, **kwargs):
        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]

class ConfigManager(metaclass=SingletonMeta):
    def __init__(self):
        self.config = {
            "app_name": "MyApp",
            "version": "1.0",
            "debug": True
        }

    def get(self, key: str):
        return self.config.get(key)

# Usage
config1 = ConfigManager()
config2 = ConfigManager()
print(config1 is config2)  # True
print(config1.get("app_name"))  # "MyApp"

# ============ MODULE-LEVEL SINGLETON (Python idiom) ============

# singleton.py
class _Logger:
    def __init__(self):
        self.log_level = "INFO"

    def log(self, message: str):
        print(f"[{self.log_level}] {message}")

# Create singleton instance at module level
logger_instance = _Logger()

# In other files, import:
# from singleton import logger_instance

# ============ LAZY SINGLETON WITH DECORATOR ============

def singleton(cls):
    """Decorator to make a class singleton"""
    instances = {}
    lock = threading.Lock()

    def get_instance(*args, **kwargs):
        if cls not in instances:
            with lock:
                if cls not in instances:
                    instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return get_instance

@singleton
class AppSettings:
    def __init__(self):
        self.theme = "dark"
        self.language = "en"

# Usage
settings1 = AppSettings()
settings2 = AppSettings()
print(settings1 is settings2)  # True

# Demo
if __name__ == "__main__":
    print("=== Basic Singleton ===")
    logger1 = Logger.get_instance()
    logger2 = Logger.get_instance()
    logger1.log("This is a log message")
    print(f"Same instance: {logger1 is logger2}\n")

    print("=== Thread-Safe Singleton ===")
    db1 = DatabaseConnection()
    db2 = DatabaseConnection()
    print(f"Same instance: {db1 is db2}\n")

    print("=== Metaclass Singleton ===")
    config1 = ConfigManager()
    config2 = ConfigManager()
    print(f"Same instance: {config1 is config2}\n")

    print("=== Decorator Singleton ===")
    settings1 = AppSettings()
    settings2 = AppSettings()
    print(f"Same instance: {settings1 is settings2}")

Key Concepts

  • Single Instance: Only one object exists
  • Global Access: Static method or property to access instance
  • Lazy Initialization: Instance created when first needed
  • Thread Safety: Double-checked locking prevents race conditions
  • Eager Loading: Instance created at class load time (faster access, slower startup)
  • Lazy Loading: Instance created on first use (slower first access, faster startup)

Eager Loading vs Lazy Loading

Eager Loading - Instance created immediately:

class EagerSingleton:
    _instance = None

    def __init__(self):
        self.data = "initialized"

    @classmethod
    def _initialize(cls):
        if cls._instance is None:
            cls._instance = cls()  # Create immediately

EagerSingleton._initialize()  # Called at module load

Lazy Loading - Instance created on first access:

class LazySingleton:
    _instance = None

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = cls()  # Create on first call
        return cls._instance

Trade-offs: Eager = predictable, no runtime overhead; Lazy = minimal startup, runtime penalty on first access.


When to Use

✅ Logger or logging service
✅ Database connection pools
✅ Configuration managers
✅ Cache managers
✅ Thread pools


Interview Q&A

Q1: Why is Singleton considered an anti-pattern?

A: 1. Global state: Hard to test and debug 2. Hidden dependencies: Class relationships unclear 3. Concurrency issues: If not thread-safe, race conditions 4. Violates SRP: Class responsible for both functionality and instance control

Better alternative: Dependency Injection

# Instead of:
logger = Logger.get_instance()

# Use:
class Service:
    def __init__(self, logger: Logger):
        self.logger = logger

Q2: Is double-checked locking thread-safe?

A: In Python, yes due to GIL (Global Interpreter Lock). In Java/C++, it's complex due to memory visibility.

# Pythonic and thread-safe
class Singleton:
    __instance = None
    __lock = threading.Lock()

    def __new__(cls):
        if cls.__instance is None:  # First check (no lock)
            with cls.__lock:
                if cls.__instance is None:  # Second check (with lock)
                    cls.__instance = super().__new__(cls)
        return cls.__instance

Q3: How do you test Singleton code?

A: Use dependency injection + mock:

# Make singletons testable
class Logger:
    __instance = None

    @classmethod
    def get_instance(cls):
        if cls.__instance is None:
            cls.__instance = cls()
        return cls.__instance

    @classmethod
    def set_instance(cls, instance):  # For testing
        cls.__instance = instance

# In tests:
mock_logger = MagicMock()
Logger.set_instance(mock_logger)

Q4: Can you have multiple singleton instances per thread?

A: Yes, use thread-local storage:

import threading

class ThreadLocalSingleton:
    _instances = threading.local()

    def __new__(cls):
        if not hasattr(cls._instances, 'instance'):
            cls._instances.instance = super().__new__(cls)
        return cls._instances.instance

# Each thread gets its own instance

Q5: How do you handle Singleton serialization/deserialization?

A:

import pickle

class SerializableSingleton:
    __instance = None

    def __new__(cls):
        if cls.__instance is None:
            cls.__instance = super().__new__(cls)
        return cls.__instance

    def __reduce__(self):
        # Ensure deserialization returns the singleton
        return (self.__class__, ())

# Usage
original = SerializableSingleton()
serialized = pickle.dumps(original)
deserialized = pickle.loads(serialized)
print(original is deserialized)  # True

Trade-offs

Pros: Global access, memory efficient, lazy initialization possible
Cons: Hidden dependencies, hard to test, violates SRP, global state complexity


Real-World Examples

  • Logger frameworks (Log4j, Python logging)
  • Database connection pools (HikariCP, psycopg2)
  • Cache managers (Redis clients)
  • Configuration servers (Spring Cloud Config)

Notes

  • Singleton class never accepts parameters, if it accepts then it becomes factory