Notes and exercises for learning design patterns
A singleton decorator gives us reusable Singleton behavior:
@singleton
class Settings:
...
But the simple decorator version has a downside:
It replaces the class name with a wrapper function.
That can make this awkward:
isinstance(settings, Settings)
It can also be less natural when we care about subclassing, type checkers, or library-style APIs.
So the problem is:
How do we control instance creation while keeping the class as a real class?
A metaclass gives us a more advanced Python answer.
The key idea is:
Do not replace the class. Change the class’s creation behavior.
A metaclass controls how classes behave.
Most Python classes are instances of type.
For example:
class Settings:
pass
Conceptually:
Settings is an object too.
The object called Settings is an instance of type.
So this is true:
print(type(Settings)) # <class 'type'>
A metaclass lets us customize what happens when the class object itself is called:
Settings()
So Singleton as a metaclass means:
Instead of replacing the class with a wrapper function, customize what happens when the class is called.
The class remains a real class.
metaclass=typeWhen you write this:
class Settings:
pass
Python behaves roughly as if you wrote this:
class Settings(metaclass=type):
pass
The second version is rarely written because type is the default metaclass.
So a normal class has this relationship:
settings instance --is instance of--> Settings
Settings class --is instance of--> type
In code:
class Settings:
pass
settings = Settings()
print(type(settings)) # <class '__main__.Settings'>
print(type(Settings)) # <class 'type'>
This is the important mental shift:
Instances are created from classes, but classes themselves are also objects created from metaclasses.
So there are two levels:
metaclass -> class -> instance
For a normal class:
type -> Settings -> settings
type?Suppose we have this normal class:
class Settings:
def __new__(cls, environment: str):
print("Settings.__new__")
return super().__new__(cls)
def __init__(self, environment: str):
print("Settings.__init__")
self.environment = environment
When we call it:
settings1 = Settings("development")
settings2 = Settings("production")
Python calls the metaclass’s __call__ method.
Because the metaclass is type, the call goes through:
type.__call__(Settings, "development")
type.__call__ roughly does this:
1. call Settings.__new__(Settings, ...)
2. call Settings.__init__(new_object, ...)
3. return the new object
So the normal flow is:
Settings("development")
|
v
type.__call__(Settings, "development")
|
v
Settings.__new__(Settings, "development")
|
v
Settings.__init__(new_object, "development")
|
v
return new_object
When we call the class a second time:
settings2 = Settings("production")
type.__call__ repeats the same process.
That means:
new call -> new object -> __init__ runs again
So this is false for a regular class:
settings1 is settings2 # False
A regular class call means:
Create a fresh instance.
From the previous section, a regular class call goes through this doorway:
type.__call__(Settings, "development")
That default doorway means:
Always create a fresh Settings object.
For Singleton behavior, we want to change that doorway so it means:
If Settings has no instance yet, create one normally.
If Settings already has an instance, return the existing one.
In other words, we want this idea:
Settings("development")
-> singleton-aware class-call behavior
-> one shared Settings object
But we should not try to modify Python’s built-in type.__call__ directly.
Instead, we create our own metaclass that inherits from type and overrides __call__.
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
Now we tell Settings to use this metaclass:
class Settings(metaclass=SingletonMeta):
def __init__(self, environment: str):
self.environment = environment
That one line changes the class-call doorway.
With a regular class:
Settings(...) -> type.__call__(Settings, ...)
With a singleton metaclass:
Settings(...) -> SingletonMeta.__call__(Settings, ...)
That is the core mechanism.
Singleton as a metaclass is not mainly changing the class body. It is changing the method that controls what happens when the class is called.
With a regular class, the relationship is:
type -> Settings -> settings instance
In code:
class Settings:
pass
settings = Settings()
print(type(settings)) # <class '__main__.Settings'>
print(type(Settings)) # <class 'type'>
With a singleton metaclass, the relationship is:
SingletonMeta -> Settings -> settings instance
In code:
class Settings(metaclass=SingletonMeta):
pass
settings = Settings()
print(type(settings)) # <class '__main__.Settings'>
print(type(Settings)) # <class '__main__.SingletonMeta'>
The instance is still a Settings instance.
So this still works:
isinstance(settings, Settings) # True
The difference is one level above the class:
Settings class --is instance of--> SingletonMeta
instead of:
Settings class --is instance of--> type
And because SingletonMeta is a subclass of type, Settings is still a normal class-like object.
It just has customized call behavior.
SingletonMeta.__call__ do?Here is the implementation again:
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
The important method is:
def __call__(cls, *args, **kwargs):
...
Here, cls is the class being called.
So when we write:
Settings("development")
Python routes the call to:
SingletonMeta.__call__(Settings, "development")
Inside __call__, cls is:
Settings
So this line:
if cls not in cls._instances:
means:
Have we already created the singleton instance for Settings?
The cache is a dictionary:
_instances = {}
The key is the class:
Settings
The value is the one shared instance of that class:
settings1
After the first call, the cache is conceptually:
{
Settings: settings1
}
That is why the same SingletonMeta can support many singleton classes.
Each class gets its own entry in the dictionary.
This line matters:
cls._instances[cls] = super().__call__(*args, **kwargs)
Since SingletonMeta inherits from type, this:
super().__call__(*args, **kwargs)
means:
Use the normal type.__call__ behavior.
So the singleton metaclass is not manually building the object from scratch. It says:
If this class has no cached instance yet, use normal Python object creation, then store the result.
The first call looks like this:
Settings("development")
|
v
SingletonMeta.__call__(Settings, "development")
|
v
No cached Settings instance exists
|
v
super().__call__("development")
|
v
type.__call__(Settings, "development")
|
v
Settings.__new__(Settings, "development")
|
v
Settings.__init__(new_object, "development")
|
v
cache and return new_object
The second call looks like this:
Settings("production")
|
v
SingletonMeta.__call__(Settings, "production")
|
v
Cached Settings instance already exists
|
v
return cached Settings object
On the second call, this does not happen again:
Settings.__new__
Settings.__init__
That is why this prints development, not production:
settings1 = Settings("development")
settings2 = Settings("production")
print(settings2.environment) # development
The first call creates and initializes the object. Later calls only retrieve it.
type versus SingletonMetaclass Settings(metaclass=type):
def __init__(self, environment: str):
self.environment = environment
Call flow:
Settings("development")
-> type.__call__
-> Settings.__new__
-> Settings.__init__
-> new Settings object
Settings("production")
-> type.__call__
-> Settings.__new__
-> Settings.__init__
-> another new Settings object
Result:
settings1 is settings2 # False
class Settings(metaclass=SingletonMeta):
def __init__(self, environment: str):
self.environment = environment
Call flow:
Settings("development")
-> SingletonMeta.__call__
-> no cached Settings instance
-> type.__call__
-> Settings.__new__
-> Settings.__init__
-> cache and return new Settings object
Settings("production")
-> SingletonMeta.__call__
-> cached Settings instance exists
-> return cached Settings object
Result:
settings1 is settings2 # True
The difference is not inside Settings.__init__.
The difference is one level above it:
the metaclass controls what calling Settings means
This example makes the difference visible.
class RegularSettings:
def __new__(cls, environment: str):
print("RegularSettings.__new__")
return super().__new__(cls)
def __init__(self, environment: str):
print("RegularSettings.__init__")
self.environment = environment
regular1 = RegularSettings("development")
regular2 = RegularSettings("production")
print(regular1 is regular2)
Output:
RegularSettings.__new__
RegularSettings.__init__
RegularSettings.__new__
RegularSettings.__init__
False
Each call creates and initializes a new object.
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
print(f"SingletonMeta.__call__ for {cls.__name__}")
if cls not in cls._instances:
print("creating new instance")
cls._instances[cls] = super().__call__(*args, **kwargs)
else:
print("returning cached instance")
return cls._instances[cls]
class SingletonSettings(metaclass=SingletonMeta):
def __new__(cls, environment: str):
print("SingletonSettings.__new__")
return super().__new__(cls)
def __init__(self, environment: str):
print("SingletonSettings.__init__")
self.environment = environment
singleton1 = SingletonSettings("development")
singleton2 = SingletonSettings("production")
print(singleton1 is singleton2)
print(singleton2.environment)
Output:
SingletonMeta.__call__ for SingletonSettings
creating new instance
SingletonSettings.__new__
SingletonSettings.__init__
SingletonMeta.__call__ for SingletonSettings
returning cached instance
True
development
Notice the second call:
SingletonSettings.__new__
SingletonSettings.__init__
are not printed again.
That is the concrete effect of overriding the metaclass __call__ method.
Here is the most important comparison.
| Part | Regular metaclass=type |
metaclass=SingletonMeta |
|---|---|---|
| Class object | Created as an instance of type |
Created as an instance of SingletonMeta |
| Class body | Normal | Normal |
| Instance type | Settings |
Settings |
isinstance(obj, Settings) |
Works | Works |
| Call target | type.__call__ |
SingletonMeta.__call__ |
| First call | Creates a new object | Creates, caches, and returns a new object |
| Later calls | Create new objects | Return cached object |
Does __init__ run every call? |
Yes | No, only when a new instance is created |
| Where is the cache? | No cache | On the metaclass, usually _instances |
The class itself stays ordinary from the user’s perspective:
settings = Settings("development")
settings.environment
isinstance(settings, Settings)
The special behavior lives above the class:
SingletonMeta.__call__
Settings.__new__?You can implement Singleton directly in the class:
class Settings:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
This works for one class.
But if many classes need the same behavior, you may duplicate the logic:
Settings
PluginRegistry
CommandRegistry
MetricsRegistry
The metaclass moves that creation rule into one reusable place:
class SingletonMeta(type):
...
Then each class opts in:
class Settings(metaclass=SingletonMeta):
...
class PluginRegistry(metaclass=SingletonMeta):
...
So the metaclass version is useful when Singleton behavior is a reusable class-level policy.
Suppose an application has a plugin registry.
Different parts of the app need to register and retrieve plugins:
auth plugins
payment plugins
export plugins
notification plugins
You want one shared registry.
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
Now the registry can use the metaclass:
class PluginRegistry(metaclass=SingletonMeta):
def __init__(self):
self._plugins = {}
def register(self, name: str, plugin):
self._plugins[name] = plugin
def get(self, name: str):
return self._plugins[name]
Usage:
plugins1 = PluginRegistry()
plugins2 = PluginRegistry()
plugins1.register("csv_export", object())
print(plugins1 is plugins2)
print(plugins2.get("csv_export"))
Output:
True
<object object at ...>
Both variables point to the same registry.
This is natural because the registry represents one shared application-wide collection.
The metaclass approach becomes more useful when several classes need the same Singleton behavior.
class PluginRegistry(metaclass=SingletonMeta):
def __init__(self):
self._plugins = {}
class CommandRegistry(metaclass=SingletonMeta):
def __init__(self):
self._commands = {}
Usage:
plugins1 = PluginRegistry()
plugins2 = PluginRegistry()
commands1 = CommandRegistry()
commands2 = CommandRegistry()
print(plugins1 is plugins2) # True
print(commands1 is commands2) # True
print(plugins1 is commands1) # False
Important detail:
Each class gets its own singleton instance.
Why?
Because the cache uses the class as the key:
_instances[PluginRegistry]
_instances[CommandRegistry]
So PluginRegistry has one instance.
CommandRegistry has one instance.
They do not share the same object.
With a singleton metaclass, the first call creates the object.
Later calls ignore their arguments because they do not create a new object.
settings1 = Settings("development")
settings2 = Settings("production")
print(settings2.environment) # development
That can be surprising.
So singleton classes should usually avoid meaningful repeated constructor arguments.
Prefer one of these designs:
class Settings(metaclass=SingletonMeta):
def __init__(self):
self.environment = load_environment()
class Settings(metaclass=SingletonMeta):
def configure(self, environment: str):
self.environment = environment
For stricter systems, the metaclass can remember the first arguments and reject conflicting later calls.
But that adds complexity, so use it only if the problem needs it.
In real backend code, two threads might try to create the singleton at the same time.
A safer version uses a lock:
from threading import Lock
class SingletonMeta(type):
_instances = {}
_lock = Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
This protects the first creation step.
The double check matters:
First check: avoid locking after the instance already exists.
Second check: avoid creating twice if two threads waited for the same lock.
For teaching, the minimal version is easier.
For production, think about concurrency.
| Question | Singleton as decorator | Singleton as metaclass |
|---|---|---|
| Where is the logic? | In a function that wraps the class | In a metaclass controlling class calls |
| Is it easy to understand? | Easier | Harder |
| Does the class remain a real class? | Not in the simple version | Yes |
Does isinstance(obj, Class) work? |
Usually not with the simple version | Yes |
| Good for one-off use? | Yes | Maybe too much |
| Good for many singleton classes? | Okay | Better |
| Supports inheritance more naturally? | No | Yes |
| Main risk | Replaces class with function | Adds metaclass complexity |
The decorator is simpler.
The metaclass is more powerful.
The key distinction:
Decorator changes what the class name points to.
Metaclass changes how the class object behaves.
Singleton is a creational pattern because it controls object creation.
The metaclass version controls creation at the class-call level:
Settings()
Instead of allowing every call to create a fresh object, it returns the cached object after the first call.
A factory centralizes object creation decisions.
A singleton metaclass also centralizes object creation behavior, but the question is different:
| Pattern | Main question |
|---|---|
| Factory | Which object should I create? |
| Singleton metaclass | Should this class create a new object or reuse its existing one? |
Factory is about choosing between alternatives.
Singleton is about preserving one identity.
Builder handles construction steps, validation, defaults, and assembly.
Singleton metaclass does not organize construction steps.
It controls repeated construction attempts.
Builder: make object construction readable and safe.
Singleton metaclass: prevent repeated construction from creating multiple instances.
Singleton can make dependencies hidden.
This is convenient:
class OrderService:
def place_order(self, order):
registry = PluginRegistry()
But the service now reaches directly into a global object.
A more testable design is often:
class OrderService:
def __init__(self, plugin_registry):
self.plugin_registry = plugin_registry
The application can still pass the singleton registry in production.
The difference is that OrderService no longer controls how the registry is obtained.
Use the metaclass version when:
you want reusable Singleton behavior across several classes
the class should remain a real class
isinstance checks should keep working
subclasses should inherit the behavior
you are comfortable with metaclasses
Good example:
class Registry(metaclass=SingletonMeta):
...
This is useful when Singleton behavior is part of a reusable class pattern.
Avoid the metaclass version when:
the team is not comfortable with metaclasses
a module-level object would be clearer
dependency injection would solve the problem
you only need one simple singleton
the extra abstraction makes the code harder to read
Metaclasses are powerful, but they are not casual tools.
If the reader has to stop and ask, “Why is there a metaclass here?”, the design should be worth that extra complexity.
Ask:
Do I need the class to remain a proper class?
Use a metaclass instead of the simple decorator.
Ask:
Do many classes need the same Singleton creation rule?
A metaclass is a good fit.
Ask:
Is this just one shared object in one module?
Prefer a module-level object.
Ask:
Am I using Singleton just to avoid passing dependencies around?
Prefer dependency injection.
Ask:
Do constructor arguments matter after the first call?
Be careful. With the simple singleton metaclass, only the first constructor call actually initializes the object.
Singleton as a metaclass means:
Keep the class as a class, but control what happens when the class is called.
The normal model is:
type -> Settings -> settings instance
The singleton metaclass model is:
SingletonMeta -> Settings -> one shared settings instance
The mental model:
Regular metaclass=type:
Settings(...) means create a new Settings object.
metaclass=SingletonMeta:
Settings(...) means ask SingletonMeta for the Settings object.
If it does not exist, create it normally and cache it.
If it already exists, return the cached one.
In one sentence:
Singleton as a metaclass is the more powerful class-creation approach: harder to teach, but better when the class must remain a real class and several classes need the same Singleton behavior.