🧠 What is Singleton Pattern?

The Singleton pattern ensures that a class has only one instance throughout the lifetime of the application and provides a global point of access to it.

🔧 When to Use It ?

Use Singleton when: You need exactly one object to coordinate actions across the system. Examples: Logging system, configuration manager, database connection pool.

🧩 Scenario

You’re building a system with two modules:

auth.py: handles user login orders.py: handles order processing

logger.py (without Singleton)

class Logger:
    def __init__(self, destination="console"):
        self.destination = destination

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

auth.py

from logger import Logger

logger = Logger(destination="console")

def login(user):
    logger.log(f"User {user} logged in")

orders.py

from logger import Logger

logger = Logger(destination="file")  # Different destination!

def place_order(order):
    logger.log(f"Order placed: {order}")

❌ Problem in this approach

In the above example you have:

1. No Guarantee of a Single Source of Truth

Each Logger() call creates a new instance, which means:

  • Settings, configuration, or state could become inconsistent.
  • You might accidentally log to different targets or formats.

➡️ Imagine if logger1 writes to a file, but logger2 writes to stdout. This inconsistency is hard to debug.

2. Hard to Maintain

When your system grows:

  • You may unknowingly pass or use different Logger instances across modules.
  • Any change (e.g., log level, output stream) must be synced across all instances manually.
  • This violates DRY and hurts maintainability.

3. Difficult to Coordinate Shared State

If the logger needs to keep track of counts, sessions, or batch info:

logger1.counter += 1
logger2.counter += 1

You’ll have two separate counter values. That’s illogical for a service meant to be centralized.

4. Breaks Intent of Centralized Services

The intent behind logging, config management, or DB pools is that they are centralized. With multiple instances:

  • You lose control.
  • You increase chances of bugs and memory waste.

Singleton Fix: Consistent Logging System

singleton_logger.py

class SingletonLogger:
    _instance = None

    def __new__(cls, destination="console"):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.destination = destination
        return cls._instance

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

auth.py

from singleton_logger import SingletonLogger

logger = SingletonLogger(destination="file")  # Set once!

def login(user):
    logger.log(f"User {user} logged in")

orders.py

from singleton_logger import SingletonLogger

logger = SingletonLogger()  # Reuses existing instance!

def place_order(order):
    logger.log(f"Order placed: {order}")

output

[FILE] User Alice logged in
[FILE] Order placed: #1234

Benefits of Using Singleton Here

  • Central configuration (log destination) managed once
  • All modules use the same logger
  • No accidental inconsistency or debugging surprises
  • Easier to refactor, mock, or replace

Singleton Implementation in Python

1. Classic Implementation Using a Class Variable

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            print("Creating new instance")
            cls._instance = super().__new__(cls)
        return cls._instance

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

logger1 = Logger()
logger2 = Logger()

print(logger1 is logger2)  # True

🔍 How It Works:

  • __new__ controls object creation.
  • Only one instance is created and reused.

2. Thread-Safe Singleton

import threading

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls, *args, **kwargs):
        with cls._lock:
            if cls._instance is None:
                cls._instance = super().__new__(cls)
        return cls._instance

This ensures safety in multi-threaded applications like web servers or microservices.

3. Pythonic Singleton via Decorator

def singleton(cls):
    instances = {}

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

    return wrapper

@singleton
class Logger:
    def log(self, message):
        print(f"[LOG]: {message}")

This is clean, reusable, and Pythonic.

Real-World Example: Configuration Manager

class Config:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.settings = {
                "db": "postgres://...",
                "mode": "production"
            }
        return cls._instance

config1 = Config()
config2 = Config()
config2.settings["mode"] = "debug"

print(config1.settings["mode"])  # debug — reflects change in the same instance

Best Practices

  • Use it for truly global, stateless services (logging, config).
  • Avoid for services that should be mockable/testable.
  • Consider dependency injection as an alternative in complex systems.