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 users

Because 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

Edge Cases and Considerations

While robust, module-based singletons have specific edge cases to consider:

  1. importlib.reload(): Calling importlib.reload() re-executes the module and creates new instances, resetting the singleton's state.
  2. Import Path Inconsistencies: If a module is imported via different absolute or relative paths (for example, import my_package.database versus import database), Python may register it twice under different keys in sys.modules, producing two distinct instances.
  3. Multiprocessing: Python processes do not share memory. Spawning multiple processes via the multiprocessing library creates a separate singleton instance per process.