Design PatternsIntermediate 12 min Lesson 1 of 13

Singleton — Exactly One Instance

One shared instance for the whole application. Learn where it genuinely helps, the testing problems it creates, and what to use instead.

Design Patterns · Lesson 1 of 13
0/13 done(0%)

What is it? #

The Singleton pattern ensures a class has exactly one instance and gives everyone access to it.

The problem it solves is real: some things should not be duplicated. A database connection pool, an application configuration, a logging setup — creating five of each wastes resources or produces inconsistent behaviour.

The problem it creates is also real: a singleton is global state with a nicer name. Any code can reach it, which makes dependencies invisible and tests order-dependent.

In Python you rarely need the classic implementation. A module is already a singleton — it is imported once and cached — so a module-level object usually does the job.

Think of it like this #

A building has one main electrical meter. Installing a second would produce two different readings for the same building, and nobody would know which to trust.

The risk is the flip side: because everyone can read the meter, nobody can tell from looking at a room which appliances depend on it.

Simple example #

An application needs one configuration object loaded from the environment at startup, and one connection pool. Both are expensive to create and must be consistent everywhere.

Code #

PYTHON
# The Pythonic approach: a module-level instance
# config.py
import os
from dataclasses import dataclass


@dataclass(frozen=True)
class Settings:
    database_url: str
    debug: bool


settings = Settings(                      # created once when the module loads
    database_url=os.environ.get("DATABASE_URL", "sqlite:///local.db"),
    debug=os.environ.get("DEBUG", "false").lower() == "true",
)

# Anywhere else:  from config import settings


# The classic implementation, when you need lazy creation
class ConnectionPool:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._ready = False
        return cls._instance

    def __init__(self, size=5):
        if self._ready:                   # __init__ still runs on every call
            return
        self.size = size
        self.connections = [f"conn-{i}" for i in range(size)]
        self._ready = True


a = ConnectionPool(size=5)
b = ConnectionPool(size=99)
print(a is b, b.size)                     # True 5 — the second size is ignored


# A cleaner lazy variant
from functools import lru_cache

@lru_cache(maxsize=1)
def get_pool(size: int = 5) -> ConnectionPool:
    return ConnectionPool(size)
TEXT
When to use it
  - one genuinely shared resource: connection pool, config, logger setup
  - creating more than one would be wrong, not just wasteful

When NOT to use it
  - to avoid passing a parameter
  - for anything holding request-specific or user-specific state
  - when tests need different instances per case
  - in multi-process deployments, where "one instance" means one per process

How it works #

The module-level version relies on Python's import system. A module executes once per process, and later imports return the cached module, so settings is created exactly once. No pattern machinery required.

The classic version overrides __new__, which runs before __init__ and controls object creation. Returning the stored instance means every call hands back the same object.

The _ready flag exists because __init__ still runs on every call, even when __new__ returned an existing object. Without the guard, ConnectionPool(size=99) would reconfigure the shared pool — a subtle and nasty bug.

The printed line shows the surprise this pattern creates: asking for a pool of 99 silently gives you the pool of 5. The second caller's intent is discarded, and nothing warns them.

The lru_cache variant expresses "create at most one" without overriding object creation, and it is easy to clear in tests with get_pool.cache_clear().

The critical detail for real deployments: a singleton is per process. Running four worker processes gives four instances, so it is never a substitute for genuinely shared state like a database or Redis.

Real-world use #

Logging configuration, connection pools, feature flag caches and application settings are the standard legitimate uses.

Most frameworks provide a better mechanism. A dependency injection container creates one instance and hands it to whoever needs it, which keeps the "one instance" property without the global access.

The testing cost is the main reason teams avoid it. State that persists between tests makes failures order-dependent and hard to reproduce, and code that reaches for a singleton internally cannot be given a substitute.

The practical compromise most teams land on: create the single instance at startup, then pass it explicitly to the code that needs it. You get one instance and visible dependencies.

Common mistakes #

  • Using a singleton to avoid passing an argument, hiding the dependency.
  • Forgetting that __init__ still runs on repeat calls and reconfiguring the shared object.
  • Storing per-request or per-user state in a singleton, leaking data between requests.
  • Assuming one instance across processes — it is one per process.
  • Ignoring thread safety during lazy creation in a multi-threaded server.

Practice #

Write a FeatureFlags singleton that loads flags once. Then rewrite it so the flags object is created at startup and passed into the two classes that use it. Write a test for each version and note which one was easier.

Quick quiz

  1. 1. What does the Singleton pattern guarantee?

  2. 2. Why is a Python module often enough?

  3. 3. Why does the classic version need a `_ready` guard?

  4. 4. What is the main testing problem with singletons?

  5. 5. How many instances exist across four worker processes?

Summary

  • Singleton guarantees one instance and a global way to reach it.
  • In Python, a module-level object usually achieves this without ceremony.
  • It is global state, which hides dependencies and complicates tests.
  • Create once at startup and pass it explicitly where possible.
  • One instance means one per process, not one per deployment.