The Singleton pattern — and why it's controversial

The pattern everyone learns early and then is told to avoid — worked through honestly, including the specific reasons it causes real problems, not just "it's considered bad practice."

Intermediate

4 min read

The problem: guaranteeing exactly one instance exists

class ConfigManager:
    def __init__(self):
        self.settings = load_settings_from_disk()   # expensive — reads a file
 
config_a = ConfigManager()   # reads the file
config_b = ConfigManager()   # reads the file AGAIN — wasteful, and now two separate copies exist

Some things genuinely should exist exactly once in a running program — application configuration loaded from disk, a connection pool, a logging setup. Nothing stops ConfigManager() from being called repeatedly, though, and each call does the expensive work again and produces an independent object, with no relationship to any other instance already created.

The pattern: control instantiation through the class itself

class ConfigManager:
    _instance = None
 
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.settings = load_settings_from_disk()
        return cls._instance
 
config_a = ConfigManager()
config_b = ConfigManager()
config_a is config_b   # True — the exact same object

__new__ is Python's actual object constructor — it runs before __init__, and is responsible for creating the raw instance. Overriding it here is what makes Singleton possible: the first call to ConfigManager() creates a real instance and stores it on the class itself (_instance); every subsequent call finds _instance already set and returns that same object instead of creating a new one. config_a is config_b being True — the identity check, not just equality — is the concrete proof that both variables refer to the literal same object in memory.

A simpler, more Pythonic way to get the same guarantee

# config.py
class _ConfigManager:
    def __init__(self):
        self.settings = load_settings_from_disk()
 
config = _ConfigManager()   # created once, when this module is first imported
# anywhere else in the codebase
from config import config
config.settings["debug"]

Python's module system already guarantees a module's top-level code runs exactly once, no matter how many times it's imported elsewhere — every from config import config gets a reference to the same object, because Python caches imported modules after their first load. This achieves the identical practical outcome as the __new__-based Singleton class — exactly one instance, shared everywhere — with less code and no need to override a dunder method. Many experienced Python developers reach for this module-level pattern instead of a formal Singleton class specifically because it's simpler and does the same job.

Why Singleton is controversial, specifically

It's global mutable state wearing a design pattern's name. A global variable is usually recognized immediately as something to be cautious about — Singleton provides the exact same capability (one shared, mutable object reachable from anywhere) but the class-based wrapping makes it look more legitimate than a bare global, even though the actual risk is the same: any code, anywhere in the program, can read or modify the shared instance's state, and tracking down which code touched it becomes genuinely difficult as the codebase grows.

It makes testing harder. A class depending directly on ConfigManager() inside its own methods (rather than receiving a config object as a constructor argument) can't be tested with a fake, controlled configuration — every test that exercises that class also exercises the real Singleton, which might have been mutated by an earlier, unrelated test, producing test failures that depend on execution order:

# Hard to test — silently depends on whatever ConfigManager's shared state currently is
class ReportGenerator:
    def generate(self):
        config = ConfigManager()
        if config.settings["debug"]:
            ...
 
# Easy to test — the dependency is explicit and injectable
class ReportGenerator:
    def __init__(self, config):
        self.config = config   # a real ConfigManager, or a fake one in tests
    def generate(self):
        if self.config.settings["debug"]:
            ...

This is exactly the Dependency Inversion problem from the SOLID lesson, showing up in a specific, common form: a class silently depending on a concrete global instance is a hard-wired dependency, hidden inside the method body rather than visible in the constructor.

It hides dependencies. Reading ReportGenerator.__init__(self, config) tells you everything that class needs to do its job. Reading ReportGenerator.generate(self) that calls ConfigManager() internally tells you nothing — the dependency on configuration is invisible from the outside, discoverable only by reading the method body.

When it's actually a reasonable, deliberate choice

A logging setup, or a resource that's expensive and genuinely must be shared for correctness (a single connection pool, so the app doesn't open hundreds of redundant database connections) are legitimate cases — the shared-single-instance requirement is real, not incidental. The problem was never "sharing one instance is always wrong" — it's specifically the global, implicitly-accessed part that causes the testing and hidden-dependency issues above. The commonly recommended middle ground: create the single instance once, near the top of the program (or via the module-import pattern above), and then pass it explicitly into whatever needs it — getting the "only one exists" guarantee without every consumer reaching out to a global to get it.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. In the __new__-based Singleton, why does `config_a is config_b` evaluate to True?

2. Why does a plain module-level instance achieve the same practical guarantee as a formal Singleton class?

3. Why does a class calling ConfigManager() directly inside its own methods make unit testing harder?

4. According to this lesson, is sharing exactly one instance itself what makes Singleton controversial?