bigonotes
CS ConceptsDesign Patterns

Singleton Pattern

Guarantee one instance with thread-safe Python singletons, how they break, and why dependency injection is often better.

8 min readLast updated Design Patterns

Some things should exist exactly once in a running application: a connection pool, a configuration manager, a logger. Singleton guarantees a class has one instance and gives everyone a single way to reach it. It's also the most overused pattern, so knowing when not to use it matters as much as knowing how.

Intent#

Ensure a class has only one instance, and provide a global point of access to it. (GoF)

Analogy: a country has one president at a time. Two simultaneous presidents would mean conflicting orders.

The problem#

Without any control, every part of the app creates its own connection:

class DatabaseConnection:
    def __init__(self):
        print("Creating expensive database connection...")


# Different parts of the app each make their own
conn1 = DatabaseConnection()
conn2 = DatabaseConnection()
conn3 = DatabaseConnection()
# Three connections: wasted resources

A global variable gives global access but doesn't stop anyone from calling the constructor again. Multiple instances waste resources and drift into inconsistent state.

Typical candidates: connection pools, loggers, configuration managers, caches, print spoolers, device drivers.

Structure#

Three ingredients:

  1. Controlled creation, so calling the class doesn't always build a new object. In Python you override __new__, since there are no private constructors.
  2. A class attribute holding the single instance (it belongs to the class, not to an object).
  3. One way in: the class call itself, or an accessor such as get_instance().
class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            # Created on the first call (lazy)
            cls._instance = super().__new__(cls)
        return cls._instance  # the same object every time


s1 = Singleton()
s2 = Singleton()
print(s1 is s2)  # True

This basic version is not thread-safe: two threads can both see None and both create an instance.

Implementation options#

There are several ways to write a singleton in Python, with different thread-safety and laziness:

Variant Thread-safe Lazy Notes
Basic __new__ No Yes Race condition on first access
Lock on every call Yes Yes Every call pays for the lock
Double-checked locking Yes Yes Lock only while creating
Module-level instance Yes No Simplest; created on first import
Cached factory function Mostly Yes @functools.cache; may build twice under a race

Lock on every call#

import threading


class Singleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        with cls._lock:  # correct, but every call takes the lock
            if cls._instance is None:
                cls._instance = super().__new__(cls)
        return cls._instance

Double-checked locking#

This is the classic answer: check without the lock, then check again with it.

import threading
from typing import Optional


class DatabaseConnectionPool:
    _instance: Optional["DatabaseConnectionPool"] = None
    _lock = threading.Lock()

    def __new__(cls, *args, **kwargs):
        # 1st check, no lock (fast path)
        if cls._instance is None:
            with cls._lock:
                # 2nd check: another thread may have won
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance._initialized = False
        return cls._instance

    def __init__(self, connection_url: str = "default_url"):
        # __init__ runs on EVERY DatabaseConnectionPool(...) call,
        # so only initialize once
        if not self._initialized:
            self.connection_url = connection_url
            self._initialized = True

    def execute_query(self, query: str) -> str:
        return f"Executing '{query}' on {self.connection_url}"


a = DatabaseConnectionPool("postgres://prod")
b = DatabaseConnectionPool("postgres://other")  # ignored: already set up
print(a is b, b.connection_url)  # True postgres://prod

The lock prevents two threads from creating two instances at the same moment; the outer check avoids taking the lock on every access.

Module-level instance (the Pythonic way)#

Python caches imported modules in sys.modules, so a module's top-level code runs only once per process. An object created there is effectively a singleton, and the import lock makes it thread-safe.

# database.py
class DatabaseConnectionPool:
    def __init__(
        self, connection_url: str = "postgresql://localhost:5432/mydb"
    ):
        self.connection_url = connection_url
        print("Initializing connection pool (happens once)")

    def execute_query(self, query: str) -> str:
        return f"Executing '{query}' on {self.connection_url}"


db_pool = DatabaseConnectionPool()  # the single shared instance
# service.py
from database import db_pool  # import the object, not the class


class UserService:
    def get_user(self, user_id: int) -> str:
        # Real code should use parameterized queries, not f-strings
        return db_pool.execute_query(
            f"SELECT * FROM users WHERE id = {user_id}"
        )


class OrderService:
    def get_order(self, order_id: int) -> str:
        return db_pool.execute_query(
            f"SELECT * FROM orders WHERE id = {order_id}"
        )

Trade-off: the instance is created at import time even if nobody uses it, imports get slower, and exceptions during creation surface as import errors.

Cached factory function#

To make the module-level approach lazy, wrap creation in a cached function:

from functools import cache


class Config:
    def __init__(self):
        print("Loading config (happens once)")


@cache
def get_config() -> Config:
    return Config()


print(get_config() is get_config())  # True

functools.cache won't corrupt itself under threads, but two threads that miss at the same moment can both run Config(). Add a lock if creation must happen exactly once.

In an interview, a good answer is to write the double-checked locking version, then mention that idiomatic Python often just uses a module-level object.

Choosing eager vs lazy#

Use eager when... Use lazy (double-checked locking or a cached function) when...
It will definitely be used It might never be used (e.g. a report generator)
Creation is cheap (loading a few properties) Creation is expensive (loading a large dataset)
Slower startup is acceptable Creation might fail (e.g. the network is down) and you want to handle it at use time

How singletons break#

Python has no private constructors, so a class-based singleton is an honor system. A few ways around it:

Attack What happens Defense
Bypassing __new__ object.__new__(Singleton) builds a second instance None really; treat it as a convention
Copying copy.copy() on a plain module-level object makes a second one Define __copy__ and __deepcopy__ to return self
Pickling Unpickling a plain object builds a new one from bytes Define __reduce__ to point back at the shared instance

A singleton built by overriding __new__ survives copy and pickle, because both call __new__ and get the existing object back (though unpickling overwrites its attributes). A plain module-level object does not.

Once there are two "singletons", each has its own state. You lose the single source of truth, resources multiply, access-control singletons can be bypassed, and locks that assumed one object stop protecting anything.

Enum members are unique by design, and copy and pickle both return the existing member, so some codebases use a one-member Enum as a singleton.

Why Singleton is often called an anti-pattern#

Singleton solves one problem but often creates others.

  • Hidden dependencies. A class that calls Logger.get_instance() inside a method doesn't show that dependency in its constructor or API.
  • Global state. Any code can change it, so bugs are hard to trace. It's a global variable with better manners.
  • Hard to test. Tests share one instance, so state leaks between them and test order starts to matter. You can't easily substitute a mock.
  • Two responsibilities. The class manages its own lifecycle and does its real job, which violates the Single Responsibility Principle.
  • Tight coupling. Every get_instance() call is hard-wired to one concrete class.

"One" only means one per process#

Each Python process gets its own singleton. Run ten replicas of a service and you have ten "singletons", each with its own state:

Service replica counter
1 100
2 45
3 78 (which one is right?)

If you truly need one thing across a distributed system, use external coordination (a database, Redis, ZooKeeper), not a singleton.

Never for per-user state#

A singleton shopping cart means every user shares one cart: user A adds a phone, user B adds a laptop, and both see both items at checkout. The same applies to user sessions, transactions and request contexts. Scope these per session or per request, or store them keyed by user ID.

Prefer dependency injection#

Instead of reaching for get_instance(), pass the shared object in through the constructor:

# Hidden dependencies
class HiddenOrderService:
    def process_order(self) -> None:
        db = Database.get_instance()
        log = Logger.get_instance()


# Explicit, testable, swappable
class OrderService:
    def __init__(self, db: "Database", log: "Logger"):
        self._db = db
        self._log = log

Your main() (or a DI container) can still make sure only one Database exists, but your classes no longer know or care. You can also hide a singleton behind an interface (an ABC or Protocol such as Logger) so tests can pass a MockLogger.

When to use it#

Use a singleton only when:

  • Exactly one instance is genuinely required: a hardware interface, a connection pool, a cache.
  • Application-wide shared resources: logging, configuration.

When not to use it#

Avoid it in these cases:

  • Only because you want global access. Inject the dependency instead.
  • For user-specific or request-specific data.
  • For stateless utilities. Plain functions (math.sqrt) are simpler.
  • When "only one" might become "several" later (per-tenant config, multiple databases).

Pitfalls#

Watch for these mistakes:

  • Lazy initialization without thread safety.
  • Forgetting the second check inside the lock in double-checked locking.
  • Storing mutable per-user data in it.
  • Making something a singleton that has no reason to be one (a Calculator with no state).

Common interview questions#

These come up often:

  • How can a singleton be broken? Bypassing __new__, copying, pickling.
  • How do you prevent it? Override __new__ (copy and pickle then return the same object), define __copy__ / __deepcopy__ / __reduce__, or use a one-member Enum.
  • Singleton vs a module of functions? A singleton is an object: it can implement an interface, be passed as a parameter, be lazily created and be swapped for a mock.

Key takeaways#

  • Controlled creation (__new__) + a class attribute holding the instance + one way in = Singleton.
  • Use a lock (double-checked locking) if several threads can create it at once.
  • A module-level instance is the idiomatic Python singleton.
  • "Single" means single per process, never across a distributed system.
  • Singletons hide dependencies and hurt testing; dependency injection usually gives you one instance without those costs.

Found a mistake? Tell me on LinkedIn ↗

Further reading#