Concepts / Creational patterns / Singleton and its alternatives

Singleton and its alternatives

Exactly one instance, and why dependency injection is usually better.

01The intuition

The problem it solves

A program has one configuration, loaded from a file, that many classes need. The first version loads it wherever it is needed.

class Config:
    def __init__(self):
        with open("settings.json") as f:
            self.values = json.load(f)

class Mailer:
    def send(self, msg):
        host = Config().values["smtp_host"]      # reads the file again
        ...

class Report:
    def render(self):
        title = Config().values["school_name"]   # and again

The file is read every time, which is slow and, worse, can produce different values if the file changes mid-run. The natural wish is "there should be exactly one Config". The singleton pattern makes a class responsible for ensuring that only one instance ever exists and for handing it out. It is also the pattern most often misused in interviews, so this article builds it and then shows the better alternative.

Build it one piece at a time

Step 1: cache the one instance on the class. The first call builds it; every later call returns the same object.

class Config:
    _instance = None

    @classmethod
    def get(cls) -> "Config":
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    def __init__(self):
        with open("settings.json") as f:
            self.values = json.load(f)

Step 2: make it safe when two threads call at once. Without a lock, two threads can both see None and build two instances. A lock around the check fixes that; checking twice avoids paying for the lock after the first time.

import threading

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

    @classmethod
    def get(cls) -> "Config":
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = cls()
        return cls._instance

Step 3: notice what the singleton did to your tests. Mailer now calls Config.get() inside its method. A test that wants a different SMTP host must mutate the global instance, and tests that run in parallel fight over it. The hidden dependency is the real cost.

Step 4: keep "one instance" and drop "global access". Build the object once in main and hand it to whoever needs it. This is dependency injection. Oneness is a decision of the wiring code, not a property enforced by the class.

class Mailer:
    def __init__(self, config: Config):
        self._config = config
    def send(self, msg):
        host = self._config.values["smtp_host"]

def main():
    config = Config()                 # created exactly once, here
    mailer = Mailer(config)
    report = Report(config)

def test_mailer_uses_host():
    fake = Config.__new__(Config); fake.values = {"smtp_host": "test.local"}
    assert Mailer(fake).host() == "test.local"
StepInstances at runtimeThread safeMailer's dependency visible in its signatureTests can substitute
Startmanyn/anono
1one, usuallynonoonly by mutating a global
2oneyesnoonly by mutating a global
4one, by wiringyesyesyes

Why it works

The classic singleton guarantees two things at once: at most one instance, and a global way to reach it. The first is often wanted; the second is what causes trouble, because every class that calls get() has a dependency you cannot see from its constructor and cannot replace. The double-checked lock is correct in Python because assignment of a reference is atomic and the second check runs under the lock.

Where it breaks: tests that leak state between each other through the global, two different configurations needed in one process (a migration tool reading two environments), and the hidden order-of-initialisation problems when singletons depend on each other. Each is solved by the injected version, which keeps the one-instance guarantee where it belongs, in the composition root.

The trade-off you must say out loud

Say both halves: "I want one instance; I do not want global access". If the interviewer asks for a singleton, implement the thread-safe form, then say what you would do in a real codebase and why, in one sentence about tests. The follow-up is usually "when is a true singleton justified", and the fair answers are a process-wide resource that genuinely cannot be duplicated, such as a hardware handle or a logging sink, and even then injected at the edges.

Where it shows up in the 100 Google problems

02Used in these problems