Design Patterns in Python
Topic 7 of 7, with 3 concept checks. Pythonic Singleton, Factory, and Observer
Apply patterns only where they simplify change
Design tradeoffs
Recognize the pressure a design pattern addresses, translate it into Python's language features, and reject ceremony that makes a small interview solution harder to explain.
Core lesson 01
Python modules are already singletons by nature (imported once, cached in sys.modules), so a module-level instance is often the simplest 'singleton' — formal class-based approaches exist too but aren't always necessary.
In languages without a true module system, Singleton classes solve the 'exactly one instance, globally accessible' problem. Python already gives you that for free: a module is loaded once and cached, so config.py defining settings = Settings() at module level IS effectively a singleton, importable everywhere as from config import settings. Formal class-based singletons (via __new__) are still used, especially for lazy initialization or when singleton-ness needs to be a property of the class itself.
# config.py
class _Settings:
def __init__(self):
self.debug = False
settings = _Settings() # created once, on first import
# anywhere else:
# from config import settings
# settings.debug = True -- same object everywhereWhat to remember
How is a Singleton typically implemented in Python, and is it always the best choice?
Common footguns
- Reaching for a heavyweight Singleton class pattern by default — often a plain module-level instance, or dependency injection, expresses the same intent more simply in Python.
Core lesson 02
A Factory centralizes object creation so callers don't need to know which concrete class to instantiate. In Python, this is often just a function with a few branches or a dict-based dispatch table.
In statically-typed languages, Factory patterns often require an abstract factory interface and dedicated factory classes to satisfy the type system. Python's duck typing means a simple function returning different concrete objects based on input already satisfies the pattern's intent — callers depend only on the returned object's interface, not the factory's type.
class Dog:
def speak(self): return "Woof"
class Cat:
def speak(self): return "Meow"
def animal_factory(kind):
return {"dog": Dog, "cat": Cat}[kind]()
pet = animal_factory("cat")
print(pet.speak()) # Meow -- caller never referenced Cat directlyWhat to remember
What's the Factory pattern, and how does Python's dynamic typing make it lighter-weight?
Common footguns
- Over-engineering a factory hierarchy (abstract factory classes, registries) for a handful of types where a simple dict dispatch or if/elif would be clearer.
Core lesson 03
A subject object keeps a list of subscriber callables and invokes each one when a notable event occurs — subscribers can be plain functions, bound methods, or any callable.
The core idea — decouple 'something happened' from 'here's what should happen in response' — doesn't need ceremony in Python. A Subject class holds a list of callbacks; subscribe(callback) appends to it, and notify(*args) calls each one. Because Python treats functions as first-class objects, subscribers can be anything callable, with no need for a formal Observer base class or interface.
class Subject:
def __init__(self):
self._subscribers = []
def subscribe(self, callback):
self._subscribers.append(callback)
def notify(self, event):
for callback in self._subscribers:
callback(event)
def log_event(event):
print("logged:", event)
subject = Subject()
subject.subscribe(log_event)
subject.notify("user_signed_up") # logged: user_signed_upWhat to remember
How is the Observer pattern typically implemented in Python?
Common footguns
- Forgetting to unsubscribe callbacks tied to short-lived objects — the subject's list keeps a reference to them, quietly extending their lifetime (a subtle memory leak).
Python lab
Browser Python lab
Runtime · idle
Python loads on your first run. Your code stays in this browser.
Best practices
- Favor plain functions and dict dispatch over formal design-pattern class hierarchies where Python's dynamic typing already gives you flexibility.
- Use module-level instances for simple singleton needs instead of a full Singleton class.
- Provide an unsubscribe/remove mechanism whenever you implement Observer, to avoid leaking references.
- Recognize that many 'GoF pattern' problems in Python are solved more simply with first-class functions, closures, or generators than in their original object-oriented formulations.
Apply the concept in Interview practice
Design Snake GamemediumLeetCode #353 · O(1) amortized per move
Track the snake as a deque of coordinates; move by adding a new head and popping the tail unless food was eaten, checking wall/self collisions each step.
Open problemLFU CachehardLeetCode #460 · O(1) get/put
Track frequency counts alongside an LRU-style structure per frequency bucket, evicting from the lowest-frequency, least-recently-used bucket — a good Strategy-style design exercise.
Open problemRandom Pick with WeightmediumLeetCode #528 · O(log n) per pick
Precompute a prefix-sum array of weights, then binary search for where a random value in the total range falls — Factory-esque object selection weighted by probability.
Open problemConcept checks
How is a Singleton typically implemented in Python, and is it always the best choice?
Hint
There are several ways — a module, a decorator, or overriding __new__.
Often a module-level instance is simpler and more pythonic than a formal class-based Singleton.
Answer
Python modules are already singletons by nature (imported once, cached in sys.modules), so a module-level instance is often the simplest 'singleton' — formal class-based approaches exist too but aren't always necessary.
In languages without a true module system, Singleton classes solve the 'exactly one instance, globally accessible' problem. Python already gives you that for free: a module is loaded once and cached, so config.py defining settings = Settings() at module level IS effectively a singleton, importable everywhere as from config import settings. Formal class-based singletons (via __new__) are still used, especially for lazy initialization or when singleton-ness needs to be a property of the class itself.
# config.py
class _Settings:
def __init__(self):
self.debug = False
settings = _Settings() # created once, on first import
# anywhere else:
# from config import settings
# settings.debug = True -- same object everywhereWatch out
- Reaching for a heavyweight Singleton class pattern by default — often a plain module-level instance, or dependency injection, expresses the same intent more simply in Python.
What's the Factory pattern, and how does Python's dynamic typing make it lighter-weight?
Hint
It's just a function (or method) that returns different types based on input.
No need for an abstract factory interface hierarchy — a function often suffices.
Answer
A Factory centralizes object creation so callers don't need to know which concrete class to instantiate. In Python, this is often just a function with a few branches or a dict-based dispatch table.
In statically-typed languages, Factory patterns often require an abstract factory interface and dedicated factory classes to satisfy the type system. Python's duck typing means a simple function returning different concrete objects based on input already satisfies the pattern's intent — callers depend only on the returned object's interface, not the factory's type.
class Dog:
def speak(self): return "Woof"
class Cat:
def speak(self): return "Meow"
def animal_factory(kind):
return {"dog": Dog, "cat": Cat}[kind]()
pet = animal_factory("cat")
print(pet.speak()) # Meow -- caller never referenced Cat directlyWatch out
- Over-engineering a factory hierarchy (abstract factory classes, registries) for a handful of types where a simple dict dispatch or if/elif would be clearer.
How is the Observer pattern typically implemented in Python?
Hint
A subject keeps a list of callbacks/subscribers and calls them all when something happens.
Python doesn't need a formal Observer interface — any callable works as a subscriber.
Answer
A subject object keeps a list of subscriber callables and invokes each one when a notable event occurs — subscribers can be plain functions, bound methods, or any callable.
The core idea — decouple 'something happened' from 'here's what should happen in response' — doesn't need ceremony in Python. A Subject class holds a list of callbacks; subscribe(callback) appends to it, and notify(*args) calls each one. Because Python treats functions as first-class objects, subscribers can be anything callable, with no need for a formal Observer base class or interface.
class Subject:
def __init__(self):
self._subscribers = []
def subscribe(self, callback):
self._subscribers.append(callback)
def notify(self, event):
for callback in self._subscribers:
callback(event)
def log_event(event):
print("logged:", event)
subject = Subject()
subject.subscribe(log_event)
subject.notify("user_signed_up") # logged: user_signed_upWatch out
- Forgetting to unsubscribe callbacks tied to short-lived objects — the subject's list keeps a reference to them, quietly extending their lifetime (a subtle memory leak).