top of page

Decorators: The Singleton Factory

Writer: Mic
Mic
Dec 30, 2025
5 min read

The singleton decorator—or singleton factory, depending on how you squint at it—is one of those neat, almost folkloric Python tricks that most developers eventually run into. The idea is simple: decorate a class in such a way that no matter how often you “instantiate” it, you always end up with the same single object.

When showing an LLM this blog post and tell it to visualise ... I think it understands it better than myself.

The obvious question is why you’d want to do this at all. The short answer is convenience. A singleton gives you a globally accessible object without forcing you to pass it through half your call stack or juggle module-level state by hand. You get one shared instance, guaranteed, and you can stop worrying about accidental duplication.


The Config example

As a starting point, suppose we have a handful of variables that together define our application’s configuration. Rather than scattering them across the codebase, we bundle them into a single dictionary. This gives us one coherent place where all configuration values live, are named explicitly, and can be passed around or inspected as a unit.

At this point, many functions will need to inspect that configuration in order to decide how they should behave. The most straightforward approach would be to make this explicit and require every relevant function to accept the configuration as an argument—so each function definition follows the same basic pattern.

One way to avoid touching every existing function signature is to shift that responsibility into a decorator. Instead of forcing each function to explicitly accept the configuration, the decorator can inject the relevant configuration data at call time, effectively adapting the function’s behaviour without changing how it is defined or invoked.

With that decorator in place, we can now define our function in the usual way—without any explicit configuration parameter—while still having access to the configuration when it actually runs.

But wait—didn’t we just say that we no longer need to pass CONFIG around? That’s exactly the case. The reason this works is that the function is now called in the usual way, without any extra arguments, because the decorator takes care of supplying the configuration behind the scenes.

But it behaves as if we had called

This approach works on the surface, but it comes with a number of subtle—and not so subtle—problems:

  • Every decorated function must be defined with a configuration parameter, or it will fail at runtime.

  • Mutating CONFIG works as expected, but rebinding it does not—the decorator still holds on to the original reference.

  • The name configuration is now effectively reserved: none of your functions can use an argument with that name for any other purpose.


The singleton factory

The clean way out of this is to move the configuration into a dedicated class. However, that only works if we can guarantee that every time this class is accessed, we are dealing with the same object. In other words, the configuration must behave like a singleton.

As a first step, we replace our dictionary with a simple class that mirrors its structure and intent—turning loosely grouped values into a clearly defined configuration object.

At this point, the remaining question is how to avoid passing the Config instance we create at program startup into every single function that needs it. Ideally, we’d like the configuration to be available implicitly—so functions can access it when needed, without having to thread the same object through every call. In other words, we want to use the configuration as if it were always there, even though it is still a well-defined object created exactly once.

If we then call it

Unsurprisingly, this fails straight away. Inside the ask_for_help function, the value of help is still True. The reason is simple: the object cfg created before the function call and the one created inside the function are completely independent instances.

What we actually want is stronger than “same values”. Our goal is to ensure that any configuration object created inside the function is forced to be the very same object that already exists outside its scope—not a copy, not a reinitialisation, but the exact same instance.

To achieve that, we introduce a decorator that enforces this behaviour explicitly.

There isn’t much going on inside the decorator itself. It defines a closure that holds a single variable—an instance—which is initially set to None. The decorator then replaces the class constructor with a function that checks this variable: if instance is still None, it creates a new instance of the class cls and stores it; otherwise, it simply returns the already existing instance.

The important point is that this check happens every time the class is “called”, ensuring that only one object is ever created.

With that in place, we can now decorate our class using this decorator…

But if we have the following initialisation in our code as usual

What actually happens now is that we no longer call the class constructor directly. Instead, we call the wrapper function introduced by the decorator. That function decides whether a new instance of the class should be created, or whether the already existing instance should be reused.

If we now run the same example again using the exact same commands as before, the behaviour changes as intended: every “instantiation” resolves to the same underlying object, and the configuration stays consistent across all call sites.

We can now see that even though we 'construct' an instance of the Config class again inside the function, it still reflects the change we made outside. The state is preserved because both “instances” are, in fact, the same object.

To make this precise, we can explicitly check object identity rather than just comparing values.

…and we can see that the two are the same object. They are not merely equal in terms of the data they contain; they share identity. In other words, we are not comparing two separate configurations that happen to look alike—we are interacting with one and the same instance throughout the program.


Other uses

Configuration objects are a natural fit for a singleton factory, but they’re far from the only use case. Any resource that is conceptually singular within a program—and expensive, sensitive, or stateful to duplicate—can benefit from this pattern.

Common examples include loggers, where you want all parts of the application to write to the same underlying sink; database or API connection managers, where opening multiple connections would be wasteful or outright harmful; and cache controllers, where consistency matters more than isolation. In all of these cases, the singleton factory provides a controlled access point without forcing the object to become a global free-for-all.

That said, singletons are not a silver bullet. They introduce shared state, which can make testing harder and hidden dependencies easier to accumulate if you’re not careful. For example it is not immediately obvious that our function was even dependent on the config class in the first place. The pattern works best when the “single instance” property matches the conceptual model of the problem, not just as a shortcut to avoid passing parameters around.


Comments


bottom of page