🧠 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.