Discover how to implement the Singleton design pattern in asynchronous Python using async/await. This in-depth guide covers practical patterns, real-world backend use cases.
The Singleton pattern is one of the most well-known design patterns in software development. It ensures that a class has only one instance and provides a global point of access to it. In synchronous Python, this is fairly straightforward. But when you step into the world of asynchronous programming, things get a bit trickier.
In this blog post, we’ll start with the basics of the Singleton pattern, then move into the async/await world, and finally explore advanced usage patterns and real-life backend scenarios. We’ll also highlight common pitfalls and how to avoid them.
A Singleton is a class that can only have one instance throughout the lifetime of an application. In Python, this is usually implemented by overriding the __new__
method or using metaclasses.
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
This pattern works fine when everything is synchronous. But what happens if you need to initialize the singleton asynchronously, like when making a database connection?
Asynchronous Python allows you to write non-blocking code using async
and await
. It’s perfect for I/O-bound tasks like API calls, database queries, or file operations. But here’s the catch: you can’t await
inside __new__
or __init__
, because they're not async
functions. So how do you create an async-aware Singleton?
Let’s say we want to create a Singleton that manages a database connection. The connection setup is asynchronous. For example using an async PostgreSQL client.
import asyncio
class AsyncDBConnector:
_instance = None
_lock = asyncio.Lock()
def __init__(self):
self.connection = None
@classmethod
async def get_instance(cls):
if cls._instance is None:
async with cls._lock:
if cls._instance is None:
instance = cls()
await instance._connect()
cls._instance = instance
return cls._instance
async def _connect(self):
# Simulate async DB connection
await asyncio.sleep(1)
self.connection = "Connected to DB"
get_instance
to create the instance._connect
method is async and performs the setup.
async def main():
db = await AsyncDBConnector.get_instance()
print(db.connection)
asyncio.run(main())
Let’s build a singleton config loader that reads secrets from an external async API like AWS Secrets Manager.
class ConfigManager:
_instance = None
_lock = asyncio.Lock()
def __init__(self):
self.config = {}
@classmethod
async def get_instance(cls):
if cls._instance is None:
async with cls._lock:
if cls._instance is None:
instance = cls()
await instance._load_config()
cls._instance = instance
return cls._instance
async def _load_config(self):
await asyncio.sleep(0.5) # Simulate network request
self.config = {
"DATABASE_URL": "postgresql://user:pass@host/db",
"API_KEY": "abc123xyz"
}
# Usage
async def main():
config = await ConfigManager.get_instance()
print(config.config["API_KEY"])
asyncio.run(main())
FastAPI is one of the most popular async web frameworks in Python. It thrives on non-blocking I/O and dependency injection. Let’s see how you can integrate an async Singleton pattern into a FastAPI app.
Imagine you have a Redis connection that should be initialized once and reused across all endpoints.
from fastapi import FastAPI, Depends
import asyncio
class RedisClient:
_instance = None
_lock = asyncio.Lock()
def __init__(self):
self.connection = None
@classmethod
async def get_instance(cls):
if cls._instance is None:
async with cls._lock:
if cls._instance is None:
instance = cls()
await instance._connect()
cls._instance = instance
return cls._instance
async def _connect(self):
await asyncio.sleep(1)
self.connection = "Simulated Redis Connection"
# Dependency wrapper
async def get_redis_client() -> RedisClient:
return await RedisClient.get_instance()
# FastAPI app
app = FastAPI()
@app.get("/status")
async def status(redis: RedisClient = Depends(get_redis_client)):
return {"status": "ok", "redis": redis.connection}
get_redis_client()
is declared as an async dependency.aiohttp
or httpx
.await
in __init__
This will fail:
class BadExample:
def __init__(self):
await self.setup() # SyntaxError
You can't await
inside __init__
. Use a separate async setup method.
If multiple coroutines call get_instance()
at the same time, you might end up with multiple instances. Always protect instantiation with an asyncio.Lock()
to ensure thread safety.
Sometimes it’s useful to reset or override the Singleton during testing.
@classmethod
def reset_instance(cls):
cls._instance = None
Even with Singletons, be cautious not to abuse them as global variables. It’s best to inject them where needed like as constructor args.
Delay the heavy work until it's really needed. This keeps startup time low.
Singletons in async Python aren’t hard, but they do require you to rethink how and when your object is created. The key takeaway is: move all await
logic out of __init__
and into an async method, and make sure to guard instance creation with an asyncio.Lock()
.
Used right, an async Singleton can be a great tool for managing shared resources like DB connections, config loaders, and API clients in modern backend applications.