Table of Contents
- What is the Singleton Design Pattern?
- Implementing Singleton in Python
- Pros and Cons of Singleton
- What is the Borg Design Pattern?
- Implementing Borg in Python
- Pros and Cons of Borg
- Singleton vs. Borg: Key Differences
- When to Use Singleton vs. Borg
- Common Pitfalls and Best Practices
- Conclusion
- References
What is the Singleton Design Pattern?
The Singleton pattern is a creational design pattern that ensures a class has only one instance and provides a global point of access to it. It is used when exactly one object is needed to coordinate actions across a system—for example, a database connection manager, a logger, or a configuration handler.
Core Intent:
- Restrict instantiation of a class to one object.
- Provide a centralized way to access that object.
Implementing Singleton in Python
Python offers multiple ways to implement Singleton. Below are the most common approaches:
1. Classic __new__ Override
The most straightforward method is to override the __new__ method, which controls object creation. We check if an instance already exists; if not, we create and store it.
class Singleton:
_instance = None # Class-level variable to hold the single instance
def __new__(cls):
if cls._instance is None:
# Create a new instance if none exists
cls._instance = super().__new__(cls)
# Initialize instance state here (optional)
cls._instance.value = 0
return cls._instance
# Usage
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # Output: True (both refer to the same instance)
s1.value = 42
print(s2.value) # Output: 42 (shared state via single instance)
2. Module-Level Singleton (Pythonic Approach)
In Python, modules are singletons by default: they are loaded once, and subsequent imports reuse the same module object. This makes modules a simple way to implement Singleton-like behavior without explicit class logic.
Example: config_singleton.py
# Module-level state acts as the "singleton"
app_config = {
"debug": False,
"log_level": "INFO"
}
def set_debug(debug: bool) -> None:
app_config["debug"] = debug
Usage in another file:
import config_singleton
config_singleton.set_debug(True)
print(config_singleton.app_config["debug"]) # Output: True
This approach is lightweight and leverages Python’s native behavior, but it lacks encapsulation (state is directly mutable).
3. Metaclass-Based Singleton
Metaclasses control class creation, making them a robust way to enforce Singleton behavior across subclasses.
class SingletonMeta(type):
_instances = {} # Track instances per class
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
# Create instance if not already tracked
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
# Usage with a subclass
class DatabaseConnection(metaclass=SingletonMeta):
def __init__(self, db_url: str):
self.db_url = db_url # Initialize with provided URL
# Create instances
db1 = DatabaseConnection("sqlite:///mydb.db")
db2 = DatabaseConnection("postgresql://user@db") # Ignored: uses existing instance
print(db1 is db2) # Output: True
print(db1.db_url) # Output: "sqlite:///mydb.db" (only the first init is used)
Metaclass-based Singleton ensures subclasses (e.g., DatabaseConnection) each have their own singleton instance, avoiding conflicts between different Singleton classes.
4. Thread-Safe Singleton
The classic __new__ override is not thread-safe: concurrent threads might both check _instance is None and create multiple instances. To fix this, use a lock:
from threading import Lock
class ThreadSafeSingleton:
_instance = None
_lock = Lock() # Prevent race conditions
def __new__(cls):
with cls._lock: # Ensure only one thread creates the instance
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
Pros and Cons of Singleton
Pros
- Controlled Access: Ensures a single point of access to critical resources (e.g., database connections).
- Reduced Overhead: Avoids redundant initialization of heavy objects (e.g., network clients).
- Global Coordination: Centralizes logic for shared resources (e.g., caches, loggers).
Cons
- Global State: Introduces hidden dependencies, making code harder to debug and test (e.g., tests may interfere via shared state).
- Inheritance Issues: Subclassing Singletons often requires reworking
__new__to avoid sharing the parent instance. - Pickling Problems: Serializing/deserializing Singletons can break the single-instance guarantee (pickled instances may create new objects).
What is the Borg Design Pattern?
The Borg pattern (also called the Monostate pattern) focuses on sharing state between instances rather than restricting instantiation. Unlike Singleton, Borg allows multiple distinct objects but ensures they all share the same state.
Core Intent:
- Enforce a single state across all instances of a class, even if multiple instances exist.
Implementing Borg in Python
Borg works by making all instances share the same __dict__ (the dictionary storing an object’s attributes). This is achieved by overriding __new__ to set each instance’s __dict__ to a class-level shared dictionary.
Basic Borg Implementation
class Borg:
_shared_state = {} # Class-level dictionary to hold shared state
def __new__(cls, *args, **kwargs):
# Create a new instance
instance = super().__new__(cls, *args, **kwargs)
# Set the instance's __dict__ to the shared state
instance.__dict__ = cls._shared_state
return instance
# Usage
b1 = Borg()
b2 = Borg()
print(b1 is b2) # Output: False (distinct objects)
b1.value = 42
print(b2.value) # Output: 42 (shared state via __dict__)
Borg with Metaclass
For stricter enforcement (e.g., across subclasses), use a metaclass to ensure all Borg subclasses share state:
class BorgMeta(type):
_shared_state = {} # Shared across all instances of classes using this metaclass
def __call__(cls, *args, **kwargs):
instance = super().__call__(*args, **kwargs)
instance.__dict__ = cls._shared_state
return instance
# Usage with a subclass
class ConfigBorg(metaclass=BorgMeta):
def __init__(self, config: dict = None):
if config:
self.__dict__.update(config) # Merge initial config into shared state
# Create instances with initial state
c1 = ConfigBorg({"debug": True})
c2 = ConfigBorg()
print(c1 is c2) # Output: False (distinct objects)
print(c2.debug) # Output: True (shared state from c1)
Pros and Cons of Borg
Pros
- Multiple Instances, Shared State: Allows distinct objects while ensuring state consistency (useful for APIs expecting multiple “instances”).
- Easier Subclassing: Subclasses can override methods or add state without breaking the shared state of the parent Borg class.
- Friendlier to Testing: Tests can create new instances (avoiding Singleton’s single-instance rigidity) while still sharing state when needed.
Cons
- Unintuitive Identity: Instances are distinct objects (
b1 is b2returnsFalse), which can confuse developers expecting Singleton-like identity. - Global State: Still suffers from the same global state issues as Singleton (hidden dependencies, testing conflicts).
- State Leakage: Accidental modification of shared state affects all instances, leading to hard-to-track bugs.
Singleton vs. Borg: Key Differences
| Feature | Singleton | Borg |
|---|---|---|
| Instance Count | Enforces exactly one instance. | Allows multiple distinct instances. |
| State Sharing | State is tied to the single instance. | State is shared across all instances via __dict__. |
| Instance Identity | instance1 is instance2 → True. | instance1 is instance2 → False. |
| Inheritance | Hard to subclass (risk of sharing parent instance). | Easier to subclass (subclasses can have their own _shared_state). |
| Pickling | May create new instances, breaking the guarantee. | State is preserved if _shared_state is pickled. |
| Testing | Hard to mock (single instance can’t be replaced). | Easier to test (instances can be reset/isolated). |
When to Use Singleton vs. Borg
Use Singleton When:
- You absolutely need one instance (e.g., a database connection pool that cannot have concurrent initializations).
- Identity matters (e.g., checking
isto validate the instance). - You want to restrict access to a resource (e.g., a hardware controller with exclusive access).
Use Borg When:
- You need shared state but want to allow multiple instances (e.g., configuration objects used by different modules).
- Subclassing is critical (e.g., creating specialized Borg subclasses with unique shared states).
- Testing requires isolated instances with shared state (e.g., resetting state between test cases).
Common Pitfalls and Best Practices
Pitfalls to Avoid
- Overusing Global State: Both patterns introduce global state, which can make code fragile. Prefer dependency injection for shared resources when possible.
- Thread Safety: Naive Singleton/Borg implementations may fail in multi-threaded environments (use locks to protect state initialization).
- Subclassing Without Planning: For Singleton, subclasses may accidentally share the parent instance. For Borg, ensure subclasses explicitly reset
_shared_stateif needed.
Best Practices
- Document Intent: Clearly label Singleton/Borg classes to avoid confusion (e.g.,
DatabaseSingletonorConfigBorg). - Encapsulate State: Expose state via methods (e.g.,
get_config(),set_config()) instead of direct attribute access to add validation. - Test Isolation: For testing, reset state between tests (e.g.,
Singleton._instance = NoneorBorg._shared_state = {}).
Conclusion
Singleton and Borg are powerful patterns for managing shared state in Python, but they solve distinct problems:
- Singleton enforces a single instance, ideal for exclusive resource access.
- Borg enforces shared state across multiple instances, offering flexibility for subclasses and testing.
Both patterns introduce global state, so use them judiciously. When possible, prefer dependency injection or module-level state for simpler, more maintainable code. For cases where shared state is unavoidable, choose Singleton for instance uniqueness and Borg for state-sharing with multiple instances.
References
- Gamma, E., et al. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martelli, A. (2000). Python Cookbook (1st ed.). O’Reilly Media (popularized the Borg pattern in Python).
- Python Documentation. Data Model:
__new__method. - Real Python. Python Design Patterns: Singleton.