Python Singleton Pattern with Module Imports
This article explains how Python naturally implements the singleton
design pattern using its built-in module import system. You will learn
the underlying mechanics of sys.modules, examine a concrete
implementation of a module-based singleton, and understand the core
benefits and potential edge cases of using this Pythonic approach over
traditional class-based implementations.
How the Import System Enforces Singletons
In Python, modules are singletons by default. When a module is
imported for the first time, Python executes its top-level code and
caches the resulting module object in a global dictionary called
sys.modules.
Any subsequent import statement for that same module
skips re-execution and immediately returns the cached reference from
sys.modules. Because every part of an application receives
the exact same module object in memory, any state or object initialized
within that module behaves as a singleton.
Implementing a Module-Based Singleton
The simplest way to implement this pattern is to define the state or instantiate a class directly within a module file.
Step 1: Create the Singleton Module
Create a file named database.py:
class _DatabaseConnection:
def __init__(self):
self.connected = False
def connect(self):
if not self.connected:
self.connected = True
print("Connected to database.")
def query(self, sql):
if not self.connected:
raise ConnectionError("Not connected.")
return f"Executing: {sql}"
# Instantiate the single instance upon import
db = _DatabaseConnection()Prefixing the class with an underscore indicates that it is internal to the module, discouraging users from instantiating it directly.
Step 2: Use the Singleton Across Files
In service_a.py:
from database import db
def run_service_a():
db.connect()In service_b.py:
from database import db
def run_service_b():
print(db.query("SELECT * FROM users"))In main.py:
import service_a
import service_b
service_a.run_service_a() # Prints: Connected to database.
service_b.run_service_b() # Prints: Executing: SELECT * FROM usersBecause database.py is cached in
sys.modules, service_a and
service_b share the exact same db instance.
Calling db.connect() in service_a persists the
connection state when service_b queries the database.
Advantages of Module Singletons
- Simplicity: It requires no boilerplate,
metaclasses, or overriding of
__new__. - Thread Safety on Import: Python handles module import locks internally, ensuring that module initialization is thread-safe during the first import.
- Pythonic Idiom: Modules are standard global containers in Python; relying on them adheres to the principle of using built-in mechanisms over complex design patterns.
Edge Cases and Considerations
While robust, module-based singletons have specific edge cases to consider:
importlib.reload(): Callingimportlib.reload()re-executes the module and creates new instances, resetting the singleton's state.- Import Path Inconsistencies: If a module is
imported via different absolute or relative paths (for example,
import my_package.databaseversusimport database), Python may register it twice under different keys insys.modules, producing two distinct instances. - Multiprocessing: Python processes do not share
memory. Spawning multiple processes via the
multiprocessinglibrary creates a separate singleton instance per process.