The Singleton design pattern is a software design principle that ensures a class has only one instance and provides a global point of access to that instance. This pattern is particularly useful for managing shared resources, like configurations, logging, or database connections.
In this article, we will explore the concept of Singletons in Python, learn about various implementation techniques, and examine real-world use cases.
Why Use Singletons?
Key Characteristics:
- Single Instance: Ensures that a class has only one instance.
- Global Access Point: Provides a way to access that single instance globally.
- Lazy Initialization: The instance is created only when needed.
Common Use Cases:
- Configuration Managers
- Logging Services
- Database Connections
- Thread Pools
Implementing Singletons in Python
Python offers several ways to implement the Singleton pattern. Let’s dive into each approach.
Classic Singleton Using a Class Variable
In this approach, the class itself manages the single instance using a class variable.
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls._instance
# Testing the Singleton
obj1 = Singleton()
obj2 = Singleton()
print(obj1 is obj2) # Output: True
Output:

Explanation:
- The
__new__method ensures that only one instance of the class is created. - If an instance already exists, it returns the existing instance.
Singleton Using a Decorator
A decorator can transform any class into a Singleton.
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class SingletonClass:
pass
# Testing the Singleton
obj1 = SingletonClass()
obj2 = SingletonClass()
print(obj1 is obj2) # Output: True
Inner Function: get_instance
- Parameters: Accepts
*argsand**kwargsto pass any arguments to the class constructor. - Logic:
- Check if Instance Exists: The function checks if the class
clsis already in theinstancesdictionary. - Create Instance if Missing: If the class is not present, a new instance is created and stored in
instances. - Return Instance: If the class already exists in
instances, it simply returns the stored instance.
- Check if Instance Exists: The function checks if the class
Application of the Decorator
- The
@singletondecorator is applied to theSingletonClassclass:
@singleton
class SingletonClass:
pass
This means any attempt to create an object of SingletonClass will invoke the get_instance function, enforcing the Singleton behavior.
Testing the Singleton
obj1andobj2are created as instances ofSingletonClass.- Singleton Verification: The comparison
obj1 is obj2evaluates toTruebecause bothobj1andobj2point to the same instance stored in theinstancesdictionary.
Singleton Using a Metaclass
A metaclass can enforce the Singleton behavior at the class level
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class Singleton(metaclass=SingletonMeta):
pass
# Testing the Singleton
obj1 = Singleton()
obj2 = Singleton()
print(obj1 is obj2) # Output: True
Module as a Singleton
In Python, modules are inherently Singletons. When you import a module, it is initialized only once per Python process.
# my_singleton.py
class MySingleton:
def __init__(self):
self.value = 42
singleton = MySingleton()
# main.py
from my_singleton import singleton
print(singleton.value) # Output: 42
Real-World Example: Logger Singleton
Let’s implement a logging service using the Singleton pattern.
import threading
class Logger:
_instance = None
_lock = threading.Lock() # To make it thread-safe
def __new__(cls, *args, **kwargs):
with cls._lock:
if not cls._instance:
cls._instance = super(Logger, cls).__new__(cls, *args, **kwargs)
return cls._instance
def __init__(self):
if not hasattr(self, "logs"):
self.logs = []
def log(self, message):
self.logs.append(message)
print(f"Log: {message}")
# Testing the Logger Singleton
logger1 = Logger()
logger2 = Logger()
logger1.log("First log message.")
logger2.log("Second log message.")
print(logger1.logs)
print(logger1 is logger2)
Output:

Let’s check what happens when we subclass a singleton class. Python
class SingletonClass(object):
def __new__(cls):
if not hasattr(cls, 'instance'):
cls.instance = super(SingletonClass, cls).__new__(cls)
return cls.instance
class SingletonChild(SingletonClass):
pass
singleton = SingletonClass()
child = SingletonChild()
print(child is singleton)
singleton.singl_variable = "Singleton Variable"
print(child.singl_variable)

In the above code you can see that the SingletonChild has the same instance of SingletonClass and also shares the same state. But there are scenarios, where we need a different instance, but should share the same state. This state sharing can be achieved using Borg singleton.
Borg Singleton:
Borg singleton is a design pattern in Python that allows state sharing for different instances. Let’s look into the following code.
class BorgSingleton(object):
_shared_borg_state = {}
def __new__(cls, *args, **kwargs):
obj = super(BorgSingleton, cls).__new__(cls, *args, **kwargs)
obj.__dict__ = cls._shared_borg_state
return obj
borg = BorgSingleton()
borg.shared_variable = "Shared Variable"
class ChildBorg(BorgSingleton):
pass
childBorg = ChildBorg()
print(childBorg is borg)
print(childBorg.shared_variable)
False
Shared Variable
Along with the creation of the new instance, a shared state is defined in the __new__ method. In this case, the shared state is stored in the shared_borg_state attribute, and it is maintained across all instances by being stored in the __dict__ attribute of each instance.
If you want to change the state, you can reset the shared_borg_state attribute. Let’s explore how to reset the shared state.
class BorgSingleton(object):
_shared_borg_state = {}
def __new__(cls, *args, **kwargs):
obj = super(BorgSingleton, cls).__new__(cls, *args, **kwargs)
obj.__dict__ = cls._shared_borg_state
return obj
borg = BorgSingleton()
borg.shared_variable = "Shared Variable"
class NewChildBorg(BorgSingleton):
_shared_borg_state = {}
newChildBorg = NewChildBorg()
print(newChildBorg.shared_variable)
Here, we have reset the shared state and tried to access the shared_variable. Let’s see the error.

Building a Web Crawler with the Classic Singleton Pattern
In this example, we’ll build a web crawler that utilizes the Classic Singleton pattern. The crawler will scan a webpage, collect all the links related to the same website, and download any images it finds. This implementation includes two main classes and two key functions.
- CrawlerSingleton: Implements the classic Singleton pattern to ensure only one instance of the crawler.
- ParallelDownloader: Uses threading to download images in parallel.
- navigate_site: Crawls the website to find and retrieve links belonging to the same domain, organizing them for image downloading.
- download_images: Crawls each individual link and downloads the images.
Additionally, we use two popular libraries to parse the web page: BeautifulSoup for parsing HTML, and an HTTP Client for making web requests.
Here’s the code to implement this:
Note: Be sure to run this on your local machine.
import httplib2
import os
import re
import threading
import urllib
import urllib.request
from urllib.parse import urlparse, urljoin
from bs4 import BeautifulSoup
class CrawlerSingleton(object):
def __new__(cls):
""" creates a singleton object, if it is not created,
or else returns the previous singleton object"""
if not hasattr(cls, 'instance'):
cls.instance = super(CrawlerSingleton, cls).__new__(cls)
return cls.instance
def navigate_site(max_links = 5):
""" navigate the website using BFS algorithm, find links and
arrange them for downloading images """
# singleton instance
parser_crawlersingleton = CrawlerSingleton()
# During the initial stage, url_queue has the main_url.
# Upon parsing the main_url page, new links that belong to the
# same website is added to the url_queue until
# it equals to max _links.
while parser_crawlersingleton.url_queue:
# checks whether it reached the max. link
if len(parser_crawlersingleton.visited_url) == max_links:
return
# pop the url from the queue
url = parser_crawlersingleton.url_queue.pop()
# connect to the web page
http = httplib2.Http()
try:
status, response = http.request(url)
except Exception:
continue
# add the link to download the images
parser_crawlersingleton.visited_url.add(url)
print(url)
# crawl the web page and fetch the links within
# the main page
bs = BeautifulSoup(response, "html.parser")
for link in BeautifulSoup.findAll(bs, 'a'):
link_url = link.get('href')
if not link_url:
continue
# parse the fetched link
parsed = urlparse(link_url)
# skip the link, if it leads to an external page
if parsed.netloc and parsed.netloc != parsed_url.netloc:
continue
scheme = parsed_url.scheme
netloc = parsed.netloc or parsed_url.netloc
path = parsed.path
# construct a full url
link_url = scheme +'://' +netloc + path
# skip, if the link is already added
if link_url in parser_crawlersingleton.visited_url:
continue
# Add the new link fetched,
# so that the while loop continues with next iteration.
parser_crawlersingleton.url_queue = [link_url] +\
parser_crawlersingleton.url_queue
class ParallelDownloader(threading.Thread):
""" Download the images parallelly """
def __init__(self, thread_id, name, counter):
threading.Thread.__init__(self)
self.name = name
def run(self):
print('Starting thread', self.name)
# function to download the images
download_images(self.name)
print('Finished thread', self.name)
def download_images(thread_name):
# singleton instance
singleton = CrawlerSingleton()
# visited_url has a set of URLs.
# Here we will fetch each URL and
# download the images in it.
while singleton.visited_url:
# pop the url to download the images
url = singleton.visited_url.pop()
http = httplib2.Http()
print(thread_name, 'Downloading images from', url)
try:
status, response = http.request(url)
except Exception:
continue
# parse the web page to find all images
bs = BeautifulSoup(response, "html.parser")
# Find all <img> tags
images = BeautifulSoup.findAll(bs, 'img')
for image in images:
src = image.get('src')
src = urljoin(url, src)
basename = os.path.basename(src)
print('basename:', basename)
if basename != '':
if src not in singleton.image_downloaded:
singleton.image_downloaded.add(src)
print('Downloading', src)
# Download the images to local system
urllib.request.urlretrieve(src, os.path.join('images', basename))
print(thread_name, 'finished downloading images from', url)
def main():
# singleton instance
crwSingltn = CrawlerSingleton()
# adding the url to the queue for parsing
crwSingltn.url_queue = [main_url]
# initializing a set to store all visited URLs
# for downloading images.
crwSingltn.visited_url = set()
# initializing a set to store path of the downloaded images
crwSingltn.image_downloaded = set()
# invoking the method to crawl the website
navigate_site()
## create images directory if not exists
if not os.path.exists('images'):
os.makedirs('images')
thread1 = ParallelDownloader(1, "Thread-1", 1)
thread2 = ParallelDownloader(2, "Thread-2", 2)
# Start new threads
thread1.start()
thread2.start()
if __name__ == "__main__":
main_url = ("https://codemagnet.in/")
parsed_url = urlparse(main_url)
main()
Output:
How the Output Is Saved:
- The
download_imagesfunction downloads the images in parallel by utilizing threading. When an image is found on a webpage, it checks whether the image has already been downloaded by consulting thesingleton.image_downloadedset. - If the image has not been downloaded before, it uses
urllib.request.urlretrieveto save the image to the localimagesfolder. The image is saved using itsbasename(the filename portion of the URL).
Where to Find the Saved Images:
- The images are saved in a folder named
imagesrelative to where the script is run. You can find the images in this folder after the script finishes execution.
The output (images) will be saved in the images directory inside your script’s working directory.


Singleton pattern is a design pattern in Python that restricts the instantiation of a class to one object. It can limit concurrent access to a shared resource, and also it helps to create a global point of access for a resource.
Comparing Singleton Approaches
| Approach | Pros | Cons |
|---|---|---|
| Class Variable | Simple and effective | Not thread-safe |
| Decorator | Clean and reusable | Slightly complex for debugging |
| Metaclass | Powerful and Pythonic | Advanced, harder to read |
| Module as Singleton | Simplest and Pythonic | Limited to module-level Singleton |
Conclusion
The Singleton Design Pattern is a powerful structural pattern that ensures a class has only one instance throughout the lifetime of an application. By restricting the instantiation of a class to a single object, it helps in managing shared resources and provides global access to the instance. In Python, implementing the Singleton pattern can be achieved in various ways, including using the __new__ method, decorators, or metaclasses.
Throughout this guide, we explored different approaches to implementing the Singleton pattern and its practical applications. We also examined the benefits, such as controlling resource usage and maintaining consistency across an application. However, while the Singleton pattern offers solutions in certain use cases, it is important to use it judiciously as it can introduce challenges like hidden dependencies and difficulties in testing.
In conclusion, understanding and applying the Singleton Design Pattern effectively can improve the maintainability and efficiency of your Python applications, especially when dealing with shared resources or configurations that need to be accessed globally. As with any design pattern, it is crucial to evaluate its necessity and ensure that it is the right solution for your problem.





Leave a Reply