py4u guide

Singleton vs. Borg: Python Design Pattern Alternatives

In software design, managing shared state and ensuring controlled access to resources are common challenges. Two design patterns that address these issues in Python are the **Singleton** and **Borg** (Monostate) patterns. While both aim to enforce a single source of truth, they achieve this goal through distinct mechanisms: Singleton restricts instantiation to a single object, while Borg allows multiple instances but ensures they share identical state. This blog explores the inner workings, implementations, pros, cons, and use cases of both patterns, helping you decide which to use in your Python projects.

Table of Contents

  1. What is the Singleton Design Pattern?
  2. Implementing Singleton in Python
  3. Pros and Cons of Singleton
  4. What is the Borg Design Pattern?
  5. Implementing Borg in Python
  6. Pros and Cons of Borg
  7. Singleton vs. Borg: Key Differences
  8. When to Use Singleton vs. Borg
  9. Common Pitfalls and Best Practices
  10. Conclusion
  11. 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 b2 returns False), 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

FeatureSingletonBorg
Instance CountEnforces exactly one instance.Allows multiple distinct instances.
State SharingState is tied to the single instance.State is shared across all instances via __dict__.
Instance Identityinstance1 is instance2True.instance1 is instance2False.
InheritanceHard to subclass (risk of sharing parent instance).Easier to subclass (subclasses can have their own _shared_state).
PicklingMay create new instances, breaking the guarantee.State is preserved if _shared_state is pickled.
TestingHard 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 is to 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_state if needed.

Best Practices

  • Document Intent: Clearly label Singleton/Borg classes to avoid confusion (e.g., DatabaseSingleton or ConfigBorg).
  • 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 = None or Borg._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.