Step 1 Understand it
The problem it solves. Some things should exist per program, like one configuration or one connection pool. Creating several wastes resources or causes conflicting settings.
Watch it happen
Read the animation as text
- A Singleton keeps one hidden instance and hands it out to everyone — a config object, say. get_instance() checks: is there one yet? If not, create it.
- Without care, two threads can check at the same moment, both see "none yet"…
- …and both create one. Now there are two "single" instances.
- Fix: wrap creation in a lock, and check again inside it. Thread 1 creates the instance; thread 2 waits, checks again, finds it, and returns the same one.
- The price: it is global state. It hides dependencies and makes tests share one object. Often it is cleaner to create one instance at start-up and pass it in.
Picture it
flowchart LR
A["Code A"] -->|"Config()"| G{"Instance exists?"}
B["Code B"] -->|"Config()"| G
G -->|"no, first call"| N["Create it and store it"]
G -->|"yes"| S["Return the stored one"]
N --> SThe idea
Used for a shared resource like a configuration object or a connection pool. The controls its own creation. Downsides: it's global state, hides dependencies, makes testing harder, and needs care with .
How it works, step by step
- The class keeps a hidden class-level variable for its single instance.
- When asked for an instance, it checks that variable. If it's empty, it creates the instance and stores it.
- With threads, two could check at the same moment, so creation is wrapped in a (and the variable is checked again inside the lock).
- Every later returns the stored instance.
Analogy
A country has one official government, and everyone refers to the same one.
Where the analogy breaks
You can't replace a country's government in a test, but code should be replaceable. Many teams prefer creating one instance at start-up and it, which gives 'only one' without the global.
Example
A configuration, or a connection pool created once per process.
Must-know points
- Use it for a truly single, shared resource.
- Make creation thread-safe (double-checked locking), or create the instance eagerly.
- In Python, a module is already a natural singleton.
- Often criticised as global state; prefer injecting one shared instance.
Common mistakes
- Using for things that aren't truly single, like 'the current user'.
- Hidden dependencies: code reaches for the global instead of receiving what it needs.
import threading
class Config:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock: # thread-safe creation
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.settings = {}
return cls._instance
Config() is Config() # True: always the same objectStep 2 Go further (free, optional)
Optional. The notes above are written to be enough on their own. These free resources give a second explanation and extra practice.
-
Watch
Christopher Okhravi · free on YouTube
-
Read
Refactoring.Guru, free
Step 3 Practise
Try each exercise yourself first: on paper, out loud or in code. Then open the answer and compare it with yours.
-
ExplainSay it in plain words in under a minute, with no notes.
Explain Singleton and its main criticism.
Show answer
A Singleton is a class that makes sure only one instance of itself ever exists in the program, and gives everyone the same one: the class controls its own creation, and asking for "a new one" returns the existing one. It is used for something there should genuinely be one of, such as the app's configuration or a database connection pool.
The main criticism: it is global state in disguise. Any code can reach for Config() from anywhere, so:
• dependencies are hidden: you can't tell from a class's constructor that it secretly uses the config or the database;
• testing is hard: the one instance carries state from one test to the next, and you can't easily hand a test a fake version;
• "there will only ever be one" often turns out to be false (a second database, a per-tenant config), and then every caller has to change;
• creating it safely from several threads needs care. -
DrawSketch the diagram (or class diagram) from memory.
Draw its : the stored instance and the method that returns it.
Show answer
The instance is stored on the class itself (a class-level field, shown underlined in UML), not on any object. The creation method checks it first: if it is empty, it creates the instance (inside a lock), and either way it returns the stored one.
text┌───────────────────────────────────┐ │ Config │ ├───────────────────────────────────┤ │ - _instance: Config = None │ ← the one stored │ - _lock: Lock │ instance (class-level) │ - settings: dict │ ├───────────────────────────────────┤ │ + __new__(): Config │ ← returns _instance, │ (or get_instance() in Java) │ creating it only the └───────────────────────────────────┘ first time ▲ ▲ ▲ Config() Config() Config() every caller gets the same object -
ApplyUse it in a real design question and say exactly where it fits.
Implement the thread-safe Singleton below and prove with 10 threads that only one instance is ever created.
Show answer
A barrier releases all 10 threads at the same moment, and a short sleep inside creation widens the window where a race could happen. The safe version is created exactly once and all 10 threads get the same object.
The check-lock-check-again ("double-checked locking") matters: the first check skips the lock once the instance exists (fast), and the second check, inside the lock, stops a thread that was waiting from creating a second one. Without the lock, the same test creates several instances.
pythonimport threading import time class Config: _instance = None _lock = threading.Lock() created = 0 def __new__(cls): if cls._instance is None: with cls._lock: # one thread at a time past here if cls._instance is None: # check again: another thread may have won time.sleep(0.01) # slow creation makes a race likely cls._instance = super().__new__(cls) cls._instance.settings = {} cls.created += 1 return cls._instance class UnsafeConfig: # the same, without the lock _instance = None created = 0 def __new__(cls): if cls._instance is None: time.sleep(0.01) cls._instance = super().__new__(cls) cls.created += 1 return cls._instance def ten_threads(cls): start = threading.Barrier(10) # release all 10 at the same moment got = [] def grab(): start.wait() got.append(cls()) threads = [threading.Thread(target=grab) for _ in range(10)] for t in threads: t.start() for t in threads: t.join() return cls.created, len({id(obj) for obj in got}) print(ten_threads(Config)) # (1, 1): created once, one shared object print(ten_threads(UnsafeConfig)[0] > 1) # True: without the lock, several were created -
Trade-offsSay what it costs, when not to use it, and the alternative.
Singleton or injecting one shared instance: compare testing and flexibility.
Show answer
Both give the program one shared instance. The difference is who decides that, and how other code gets hold of it.
Singleton: the class enforces "only one", and code fetches it from anywhere with Config().
• Testing: the instance is global, so state leaks between tests, and swapping in a fake means monkey-patching the class.
• Flexibility: "exactly one" is baked in; needing two later (a second database, one config per customer) means changing every caller.
• Hidden dependencies: you can't see from a constructor what a class really uses.
• Convenience: no wiring at all.Injecting one shared instance: create one Config at start-up (in main) and pass it into the classes that need it.
• Testing: each test passes its own fresh or fake instance. Nothing leaks.
• Flexibility: making two is just calling the constructor twice.
• Dependencies are visible in every constructor.
• Cost: a little wiring code.In most designs, injection is better: you get "one instance" by choice at start-up, without the downsides of the pattern.
Step 4 Interview-ready, then prove it
Your interview answer
About 20 to 40 seconds, said out loud. Practise it until it sounds like you, not like notes.
Singleton guarantees one instance with one access point, for things like configuration or a connection pool. I'd make creation thread-safe, but I usually prefer creating one instance at start-up and injecting it, because a global singleton hides dependencies and makes testing harder.
Follow-up questions
Why is Singleton sometimes called an anti-pattern?
It's global, changeable state: hidden dependencies, hard to replace in tests, and tricky lifetimes.
Tick each check when you can do it without notes
A topic is finished when you pass all four checks, not when you finish a video. Be strict: if you needed your notes, it is not ticked yet.
- Explain Say it in plain words in under a minute, with no notes.
- Draw Sketch the diagram (or class diagram) from memory.
- Apply Use it in a real design question and say exactly where it fits.
- Trade-offs Say what it costs, when not to use it, and the alternative.
0 of 4 checks ticked.
Saved in this browser. Back it up or move it to another device.