Singletons in Python

Ensuring a single instance for shared resources

The singleton pattern is a creational design pattern that ensures a class has only one instance while providing a global point of access to that instance. This post explores the concept of singletons in Python, exploring various implementation methods including naive approaches, base classes, decorators, and metaclasses. We also discuss the advantages and disadvantages of using singletons, their impact on design principles like the Single Responsibility Principle, and provide thread-safe implementations for concurrent environments.
programming
Python
tutorial
🇬🇧
Author

Antonio Montano

Published

December 8, 2023

Modified

December 11, 2023

Before diving into different implementations of a Python singleton, it’s essential to understand the concept from both theoretical and practical perspectives.

What is a design pattern?

A design pattern is a reusable solution to common problems that software developers encounter during application development. It represents best practices for addressing recurring design challenges, enabling developers to write code that is more flexible, maintainable, and reusable. Design patterns also provide a common vocabulary for designers and developers to communicate their approaches to solving software design problems.

Design patterns can be categorized into three main types:

  • Creational patterns: Focus on object creation, optimizing efficiency and controlling how instances are instantiated. Examples include singleton, Factory Method, Builder, Prototype, and Abstract Factory.

  • Structural patterns: Deal with object composition, ensuring that relationships between components are efficient and effective. Examples include Adapter, Bridge, Composite, Decorator, Facade, and Proxy.

  • Behavioral patterns: Define how objects interact and communicate with each other. Examples include Strategy, Observer, Command, State, and Iterator.

What is a creational design pattern?

A creational design pattern focuses on how objects are created, helping developers manage complex instantiation processes in a more adaptable and reusable manner. By abstracting the object creation process, creational patterns allow the code to be more flexible and maintainable. Some common creational design patterns include:

  • Factory Method: Defines an interface for creating an object but lets subclasses decide the specific type of object to create.

  • Abstract Factory: Provides an interface for creating families of related objects without specifying their concrete classes.

  • Builder: Separates the construction of a complex object from its representation, allowing the construction process to produce different outcomes.

  • Prototype: Creates new objects by copying an existing instance, which serves as a prototype.

  • Singleton: Ensures that a class has only one instance while providing a global access point to that instance.

More on singletons

The singleton pattern serves two primary purposes:

  1. Ensure that a class has only one instance: The main goal of the singleton pattern is to control the number of instances of a class. This pattern is particularly useful when managing shared resources, such as database connections or configuration files. If an object already exists, any subsequent request to create the class should return the existing instance.

    This behavior cannot be easily implemented using a typical constructor, as constructors are designed to return new objects each time they are invoked.

  2. Provide a global access point to the instance: The singleton pattern also allows the instance to be accessed globally, similar to a global variable. However, unlike global variables, the singleton pattern ensures that the instance cannot be accidentally overwritten or modified by other parts of the code, reducing the risk of errors and crashes.

    By encapsulating the logic that guarantees a single instance within the class itself, the code remains more organized and consistent.

So, the singleton pattern is especially helpful when dealing with shared resources, where having multiple instances would lead to inefficiency, resource conflicts, or inconsistencies.

Naive implementation

A basic singleton can be implemented by keeping track of whether an instance has already been created. If an instance exists, any request for a new instance will return the existing one. This approach provides a global point of access to shared resources. Here is a simple implementation of a singleton in Python:

class Logger:
1  _instance = None

  def __new__(cls, *args, **kwargs):
2    if not cls._instance:
3      cls._instance = super(Logger, cls).__new__(cls, *args, **kwargs)

4    return cls._instance

  def __init__(self):
5    if not hasattr(self, 'log'):
      self.log = []

  def write_log(self, message):
    self.log.append(message)

  def read_log(self):
    return self.log

# Example usage
logger1 = Logger()
logger2 = Logger()

logger1.write_log("Log message 1")
6print(logger2.read_log())

7print(logger1 is logger2)
1
Initializes the class variable _instance to None to store the singleton instance.
2
Checks if _instance is None, meaning no instance has been created yet.
3
Creates a new instance using super().__new__ and assigns it to _instance.
4
Returns the singleton instance.
5
Checks if the log attribute is already initialized to prevent re-initialization.
6
Output: ['Log message 1'].
7
Output: True.

In this example, the Logger class ensures that only one instance is created by overriding the __new__ method. When logger1 and logger2 are instantiated, they both reference the same instance, demonstrating the singleton pattern in action.

Disadvantages

While the singleton pattern offers advantages like centralized control and resource efficiency, it also has some notable downsides:

  • Increased Coupling: Since singletons provide a global point of access, they can increase coupling between different components of an application. This can make refactoring or isolating parts of the system for testing more challenging.

  • Global State: Introducing a global state can lead to unpredictable behavior, making it difficult to track changes and understand the state of the application.

  • Testing Challenges: Singletons can complicate testing because enforcing a single instance can make it difficult to create isolated testing scenarios or simulate different states.

Due to these potential issues, some developers view the singleton pattern as an antipattern and recommend using it only when its benefits outweigh the disadvantages.

Single Responsibility Principle

The Single Responsibility Principle (SRP) is one of the SOLID principles1 of object-oriented design. It states that a class should have only one reason to change, meaning it should have a single responsibility or function within the system.

1 The SOLID principles are a set of five fundamental design guidelines aimed at making software more understandable, flexible, and maintainable. The acronym SOLID stands for: Single Responsibility Principle, Open/Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), Dependency Inversion Principle (DIP). These principles were introduced by Robert C. Martin, also known as Uncle Bob, in the early 2000s. They have become a cornerstone in object-oriented design and programming, promoting best practices that help developers build robust and scalable software systems.See Martin, R. C. (2002). Agile Software Development, Principles, Patterns, and Practices. Prentice Hall. ISBN: 978-0135974445.

Singletons often violate SRP because they typically serve two distinct purposes:

  1. Managing their own instance: The singleton pattern ensures that only one instance of a class is created, and this instance management logic is built into the class itself.

  2. Providing functional behavior: In addition to managing their own instance, singleton classes also provide functionality related to their main purpose (e.g., logging, configuration management, etc.).

By combining both instance management and functional behavior, a singleton class is taking on more than one responsibility, which violates SRP. This dual responsibility can lead to increased complexity, reduced maintainability, and challenges when attempting to test or extend the class.

A real-world application

One of the simplest ways to implement the singleton pattern is by using a class-level attribute to store the instance. This method is both straightforward and effective.

Consider a scenario where we want to manage application-wide logging. The singleton pattern ensures that all parts of the application use the same logger object:

class Logger:
1  _instance = None

  def __new__(cls, *args, **kwargs):
2    if cls._instance is None:
3      cls._instance = super(Logger, cls).__new__(cls, *args, **kwargs)
4      cls._instance.log = []

5    return cls._instance

  def write_log(self, message):
    self.log.append(message)

  def read_log(self):
    return self.log

# Example usage
logger1 = Logger()
logger2 = Logger()

6print(logger1 is logger2)

logger1.write_log("Log message 1")
7print(logger2.read_log())
1
Initializes the class variable _instance to None to store the singleton instance.
2
Checks if _instance is None to determine if an instance already exists.
3
Creates a new instance using super().__new__ and assigns it to _instance.
4
Initializes the log attribute with an empty list.
5
Returns the singleton instance.
6
Output: True.
7
Output: ['Log message 1'].

In this example, the Logger class is used to manage application-wide logging. The __new__ method ensures that only one instance of the class is created. If an instance already exists, it is returned; otherwise, a new instance is created. This approach is effective and easy to understand, making it a good choice for simpler use cases.

Alternative implementations

The singleton pattern can be implemented in several ways in Python, including using a base class, a decorator, or even a metaclass.

Using a base class

One way to implement the singleton pattern is by using a base class that other classes inherit from. This base class defines the singleton behavior, ensuring that only one instance of the derived class is created.

class SingletonBase:
1  _instances = {}

  def __new__(cls, *args, **kwargs):
2    if cls not in cls._instances:
3      cls._instances[cls] = super(SingletonBase, cls).__new__(cls, *args, **kwargs)

4    return cls._instances[cls]

# Example usage
class Logger(SingletonBase):
  def __init__(self):
5    if not hasattr(self, 'log'):
      self.log = []

  def write_log(self, message):
    self.log.append(message)

  def read_log(self):
    return self.log

# Testing the singleton behavior
logger1 = Logger()
logger2 = Logger()

logger1.write_log("Log message 1")
6print(logger2.read_log())

7print(logger1 is logger2)
1
Initializes a class-level dictionary _instances to keep track of singleton instances.
2
Checks if the class (cls) is not in _instances to determine if an instance has been created.
3
Creates a new instance and stores it in _instances under the class key.
4
Returns the singleton instance from _instances.
5
Checks if the log attribute is already initialized to prevent re-initialization.
6
Output: ['Log message 1'].
7
Output: True.

In this implementation:

  • The SingletonBase class ensures that only one instance of any subclass is created by maintaining a dictionary (_instances) of instances.

  • The Logger class inherits from SingletonBase, resulting in shared behavior and a single instance.

This approach is useful when multiple classes need to follow the singleton pattern, allowing for reuse of the singleton logic.

Using a decorator

What is a decorator?

A decorator in Python is a function that allows you to modify the behavior of another function or class without changing its code. Decorators provide a clean, readable way to extend functionality by “wrapping” a function or class, making it easy to add behavior dynamically.

Decorators are commonly used in web frameworks to handle concerns like authentication, logging, and caching. They are an effective way to separate cross-cutting concerns from the main logic of a function, leading to more organized and maintainable code.

Below is an example of using a decorator to log function calls:

1def log_decorator(func):
2  def wrapper(*args, **kwargs):
3    print(f"Calling function '{func.__name__}' with arguments {args} and {kwargs}")
4    result = func(*args, **kwargs)
5    print(f"Function '{func.__name__}' returned {result}")

6    return result

7  return wrapper

8@log_decorator
9def add(a, b):
10  return a + b

# Using the decorated function
11add(3, 5)
1
Defines the log_decorator function, which accepts another function func as its argument.
2
Defines an inner function wrapper that can accept any number of positional (*args) and keyword (**kwargs) arguments.
3
Prints a message indicating that func is being called, along with the arguments passed to it.
4
Calls the original function func with the provided arguments and stores the result in result.
5
Prints a message indicating that func has returned a value, displaying the result.
6
Returns the result obtained from calling func.
7
Returns the wrapper function, effectively replacing func with wrapper.
8
Applies the log_decorator to the add function using the decorator syntax.
9
Defines the add function, which takes two arguments a and b.
10
Returns the sum of a and b.
11
Calls the decorated add function with arguments 3 and 5.

Code

To implement the singleton pattern using decorators, we create a decorator function that wraps a class, ensuring that only one instance of that class is created.

Step 1: Create a wrapper class

The wrapper class is responsible for storing the instance of the decorated class and ensuring that any subsequent requests return the same instance.

class SingletonInstanceWrapper:
  def __init__(self, cls):
1    self.__wrapped__ = cls
2    self._instance = None

  def __call__(self, *args, **kwargs):
3    if self._instance is None:
4      self._instance = self.__wrapped__(*args, **kwargs)

5    return self._instance
1
Stores the original class in the __wrapped__ attribute.
2
Initializes _instance to None to hold the singleton instance.
3
Checks if _instance is None to determine if an instance needs to be created.
4
Creates a new instance of the decorated class and assigns it to _instance.
5
Returns the singleton instance.
Step 2: Create the decorator function

Next, we need a decorator function that returns an instance of the wrapper class. This function will make it easy to apply the singleton pattern to any class by simply adding a decorator.

def ensure_single_instance(cls):
  return SingletonInstanceWrapper(cls)
Step 3: Use the decorator

We can now use the decorator to enforce singleton behavior on any class. Let’s apply it to a Logger class to see how it works:

@ensure_single_instance
class Logger:
  def __init__(self):
    self.log = []

  def write_log(self, message):
    self.log.append(message)

  def read_log(self):
    return self.log

# Example usage
logger1 = Logger()
logger2 = Logger()

logger1.write_log("Log message 1")
1print(logger2.read_log())

2print(logger1 is logger2)
1
Output: ['Log message 1'].
2
Output: True.

In this example, the Logger class is decorated with @ensure_single_instance. As a result, both logger1 and logger2 refer to the same instance, demonstrating the singleton behavior.

This approach highlights the power of combining decorators with the singleton pattern. By adding the @ensure_single_instance decorator, we ensure that the Logger class functions as a singleton, with all instances referring to the same underlying object. This simplifies the code and makes the intent explicit, enhancing readability and maintainability.

Using a metaclass

A metaclass can also be used to implement the singleton pattern. A metaclass is a class of a class, meaning it defines how classes behave. By using a metaclass, you can control the instantiation process of classes, making it a suitable tool for enforcing the singleton pattern.

Below is an example of how to implement the singleton pattern using a metaclass:

class SingletonMeta(type):
1  _instances = {}

  def __call__(cls, *args, **kwargs):
2    if cls not in cls._instances:
3      cls._instances[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs)

4    return cls._instances[cls]

# Example usage
class Logger(metaclass=SingletonMeta):
  def __init__(self):
5    if not hasattr(self, 'log'):
      self.log = []

  def write_log(self, message):
    self.log.append(message)

  def read_log(self):
    return self.log

# Testing the singleton behavior
logger1 = Logger()
logger2 = Logger()

logger1.write_log("Log message 1")
6print(logger2.read_log())

7print(logger1 is logger2)
1
Initializes a class-level dictionary _instances to store instances of classes using this metaclass.
2
Checks if the class (cls) is not in _instances to see if an instance has been created.
3
Creates a new instance using super().__call__ and stores it in _instances.
4
Returns the singleton instance from _instances.
5
Checks if the log attribute is already set to avoid re-initialization.
6
Output: ['Log message 1'].
7
Output: True.

In this implementation:

  • The SingletonMeta class is a metaclass that overrides the __call__ method. This method is responsible for creating instances of classes.

  • The __call__ method checks if an instance already exists in the _instances dictionary. If not, it creates a new instance and stores it. Otherwise, it returns the existing instance.

  • The Logger class uses SingletonMeta as its metaclass, ensuring that only one instance is ever created.

This approach is particularly powerful because it allows you to enforce singleton behavior at the metaclass level, meaning that any class using SingletonMeta as its metaclass will automatically follow the singleton pattern. This approach is also more flexible and reusable compared to other singleton implementations.

Using metaclasses for singletons allows for a more Pythonic approach to instance management, especially when working with multiple classes that need to follow the singleton pattern.

Comparing the three implementations

Each of the three implementations of the singleton pattern—using a base class, a decorator, and a metaclass—has its own advantages and use cases:

  • Base class implementation: This approach is useful when multiple classes need to follow the singleton pattern. It allows for reuse of the singleton logic, as any class inheriting from the base class will automatically follow the singleton behavior. However, it introduces tight coupling with the base class, which might limit flexibility.

  • Decorator implementation: The decorator approach makes the intent to create a singleton explicit in the class definition. It keeps the singleton logic separate from the core functionality of the class, promoting better separation of concerns. This method is highly readable, but requires a decorator function and an additional wrapper class, which can add some complexity.

  • Metaclass implementation: Using a metaclass to enforce the singleton pattern is a powerful and Pythonic solution. It allows multiple classes to follow the singleton pattern without explicit inheritance or decoration. This approach is highly reusable and works well when you need singleton behavior across different classes without modifying each class definition. However, metaclasses can be more difficult to understand, especially for developers who are not familiar with Python’s metaclass system.

Taking into account thread-safety

It’s crucial to understand when explicit thread safety management is needed, as it comes with a computational cost. In Python, the Global Interpreter Lock (GIL) ensures that only one thread executes Python bytecode at a time, which can mitigate the need for additional thread safety in simpler scenarios. However, more advanced data structures involving non-atomic operations still require explicit thread safety with locks to prevent issues when multiple threads are accessing or modifying shared resources.

To make singleton implementations thread-safe, we need to ensure that multiple threads do not create multiple instances simultaneously. Below are thread-safe versions of the singleton pattern implemented using a base class, a decorator, and a metaclass.

Using a base class

In a thread-safe singleton implementation using a base class, we use a lock to ensure that only one thread can create the instance at a time:

1import threading

class SingletonBaseThreadSafe:
  _instances = {}
2  _lock = threading.Lock()

  def __new__(cls, *args, **kwargs):
    if cls not in cls._instances:
3      with cls._lock:
        if cls not in cls._instances:
4          cls._instances[cls] = super(SingletonBaseThreadSafe, cls).__new__(cls, *args, **kwargs)

    return cls._instances[cls]

# Example usage
class Logger(SingletonBaseThreadSafe):
  def __init__(self):
5    if not hasattr(self, 'log'):
      self.log = []

  def write_log(self, message):
    self.log.append(message)

  def read_log(self):
    return self.log

# Testing the singleton behavior
logger1 = Logger()
logger2 = Logger()

logger1.write_log("Log message 1")
6print(logger2.read_log())

7print(logger1 is logger2)
1
Imports the threading module to use threading locks.
2
Initializes a class-level lock _lock to ensure thread safety.
3
Acquires the lock to prevent multiple threads from entering the critical section.
4
Creates the singleton instance inside the locked section if it doesn’t exist.
5
Checks if the log attribute is already initialized.
6
Output: ['Log message 1'].
7
Output: True.

In this implementation:

  • A class-level lock (_lock) is used to ensure that only one thread can execute the code that creates the singleton instance.

  • The with cls._lock statement prevents multiple threads from entering the critical section where the instance is created, ensuring thread safety.

Using a decorator

The decorator-based singleton can be made thread-safe by adding a lock to ensure only one thread creates the instance:

1import threading

class SingletonInstanceWrapperThreadSafe:
2  _lock = threading.Lock()

  def __init__(self, cls):
    self.__wrapped__ = cls
    self._instance = None

  def __call__(self, *args, **kwargs):
    if self._instance is None:
3      with self._lock:
        if self._instance is None:
4          self._instance = self.__wrapped__(*args, **kwargs)

    return self._instance

def ensure_single_instance_thread_safe(cls):
  return SingletonInstanceWrapperThreadSafe(cls)

# Example usage
@ensure_single_instance_thread_safe
class Logger:
  def __init__(self):
    self.log = []

  def write_log(self, message):
    self.log.append(message)

  def read_log(self):
    return self.log

# Testing the singleton behavior
logger1 = Logger()
logger2 = Logger()

logger1.write_log("Log message 1")
5print(logger2.read_log())

6print(logger1 is logger2)
1
Imports the threading module.
2
Initializes a class-level lock _lock to manage thread access.
3
Acquires the lock to enter the critical section safely.
4
Creates the singleton instance within the locked section if it doesn’t exist.
5
Output: ['Log message 1'].
6
Output: True.

In this implementation:

  • A class-level lock (_lock) is used to prevent multiple threads from creating multiple instances simultaneously.

  • The with self._lock statement ensures that only one thread can execute the code that initializes the singleton instance.

Using a metaclass

For a thread-safe singleton using a metaclass, we add a lock to the metaclass to ensure that only one thread can create the instance:

1import threading

class SingletonMetaThreadSafe(type):
  _instances = {}
2  _lock = threading.Lock()

  def __call__(cls, *args, **kwargs):
    if cls not in cls._instances:
3      with cls._lock:
        if cls not in cls._instances:
4          cls._instances[cls] = super(SingletonMetaThreadSafe, cls).__call__(*args, **kwargs)

    return cls._instances[cls]

# Example usage
class Logger(metaclass=SingletonMetaThreadSafe):
  def __init__(self):
5    if not hasattr(self, 'log'):
      self.log = []

  def write_log(self, message):
    self.log.append(message)

  def read_log(self):
    return self.log

# Testing the singleton behavior
logger1 = Logger()
logger2 = Logger()

logger1.write_log("Log message 1")
6print(logger2.read_log())

7print(logger1 is logger2)
1
Imports the threading module.
2
Initializes a class-level lock _lock in the metaclass.
3
Uses the lock to prevent concurrent instance creation.
4
Creates the singleton instance inside the critical section.
5
Checks if the log attribute is already set to avoid re-initialization.
6
Output: ['Log message 1'].
7
Output: True.

In this implementation:

  • The metaclass SingletonMetaThreadSafe uses a class-level lock (_lock) to prevent multiple threads from creating multiple instances.

  • The with cls._lock statement ensures thread safety by restricting access to the instance creation code to only one thread at a time.

Summary

All three implementations ensure that the singleton instance is created in a thread-safe manner by using locks. This prevents multiple threads from creating separate instances, ensuring the singleton property holds even in concurrent environments.

In CPython, the reference implementation of Python, the GIL ensures that only one thread executes Python bytecode at a time. This means that even without explicit locks, bytecode execution is atomic at the interpreter level, which can mitigate some thread safety concerns for simple operations. However, the GIL does not protect against all threading issues, especially when dealing with non-atomic operations or when interfacing with external systems and I/O operations. Therefore, relying solely on the GIL for thread safety is not advisable.

Moreover, there are proposals like PEP 703 titled Making the Global Interpreter Lock Optional in CPython, which aim to make the GIL optional in future versions of Python. If such changes are implemented, threads could execute Python bytecode concurrently, removing the atomicity guarantees currently provided by the GIL. This would increase the importance of explicit thread safety mechanisms in your code.

Given these considerations, it’s important to implement explicit thread safety measures, such as locks, in your singleton implementations. This ensures that your code is robust not only in the current CPython environment but also in future Python interpreters that may not have a GIL. By proactively managing thread safety, you can prevent subtle bugs and race conditions that could occur in a truly concurrent execution environment.

While each singleton implementation method—base class, decorator, or metaclass—has its own strengths, the choice depends on the specific requirements of your application, such as readability, reusability, and your familiarity with Python’s advanced features like metaclasses or decorators. Regardless of the method chosen, incorporating explicit thread safety measures is crucial for maintaining the singleton property in multi-threaded applications, both now and in anticipation of future developments in Python’s concurrency model.

Final thoughts

The singleton pattern is a powerful tool when used appropriately, particularly for managing shared resources like configuration settings or logging mechanisms. However, it’s important to weigh the benefits of the singleton pattern against its potential downsides. Overusing it or applying it in the wrong context can lead to design issues such as increased coupling, global state management problems, and violations of the Single Responsibility Principle.

The singleton pattern can be implemented in several ways, each with its pros and cons. The base class implementation is straightforward and easy to reuse but can introduce tight coupling. The decorator implementation provides clear separation of concerns and is highly readable but may add complexity due to the need for additional wrapper classes. The metaclass approach is powerful and reusable across different classes without modifying their definitions, but it may be challenging for developers who are not familiar with metaclasses.

In summary, while the singleton pattern is useful, its usage should be carefully considered and limited to cases where ensuring a single instance truly adds value to the application. Understanding the trade-offs of different implementations will help you make the best design decisions for your specific needs.

Back to top