Python Design Patterns — Singleton Connection Pool Leak
After a microservice deployment, a Singleton duplicate pool opened 3x connections — find out why dynamic imports broke the pattern and fix it.
- Design patterns are reusable solutions to recurring software design problems
- Singleton: one instance per process, often for configuration or connection pooling
- Factory: decouples object creation from business logic, enabling swap of implementations
- Observer: publish-subscribe that keeps components loosely coupled but can leak memory
- Strategy: lets you swap algorithms at runtime, reduces if/elif chains
- Performance insight: Singleton with module-level caching beats class-based singleton by ~40% due to no lock overhead
- Production insight: Observer with weak references still leaks if callbacks hold closures over large object graphs
- Biggest mistake: using Singleton as a global variable — it hides dependencies and kills testability
Imagine you're building a LEGO city. Instead of figuring out from scratch how to build every house, shop, and bridge, LEGO gives you instruction booklets — proven blueprints that always work. Python design patterns are exactly that: battle-tested blueprints for solving common software problems. You don't have to invent the wheel every time a new coding challenge appears. You just pick the right booklet, adapt it to your city, and keep building.
Every senior Python developer has felt it — that sinking moment when a codebase that worked perfectly at 1,000 users starts buckling at 100,000. New engineers can't follow the logic, a shared resource keeps getting instantiated twenty times, and event-driven code turns into an untraceable chain of callbacks. Design patterns are the vocabulary that lets experienced engineers name these problems and reach for proven solutions without reinventing them under pressure. They're not academic exercises — they're the difference between a codebase you're proud to hand off and one you're embarrassed to demo.
The real problem design patterns solve isn't complexity — it's communication and entropy. As a codebase grows, the same structural problems appear over and over: how do you ensure exactly one database connection pool exists? How do you add features to an object without editing its class? How do you notify dozens of components when state changes without coupling them tightly together? Without patterns, every developer on your team invents a slightly different answer, and the codebase quietly fractures. Patterns give you a shared language and a proven structure before the chaos sets in.
By the end of this article you'll understand the internal mechanics of Singleton, Factory, Observer, Strategy, and Decorator patterns in Python — not just the textbook definitions but the gotchas that bite production systems, the performance trade-offs that matter at scale, and how Python's own language features (metaclasses, descriptors, __call__, functools) interact with these patterns in ways that surprise even experienced devs. You'll be able to choose the right pattern for the right problem and explain your reasoning in a technical interview.
Why Design Patterns Matter in Python — Beyond the Hype
Design patterns are not about fancy UML diagrams or memorising GOF names. In Python, many patterns are built into the language itself: decorators, generators, context managers, and even the module system. But that doesn't make the patterns obsolete. It makes them more powerful — and more dangerous when applied carelessly.
Take the Singleton pattern. Python's module system gives you a Singleton for free: a module is imported once, and its top-level names persist for the process lifetime. But many teams still write a class-based Singleton and get it wrong — they forget thread safety, or they assume the initialisation in __new__ runs only once when it actually runs on every instance creation attempt. The result? A Singleton that creates multiple instances under concurrent load.
Patterns in Python aren't about how to implement them — they're about when to use them and how to adapt them to Python's idioms. The real value comes from understanding the trade-offs: a module-level Singleton is simple but untestable; a dependency-injected Singleton is testable but requires a container. Knowing which trade-off to accept for your context is what separates senior engineers from juniors.
- Without patterns, every code review turns into a debate: 'Why did you put this logic here?' With patterns, you say: 'This is the Factory pattern. It lives in the factory module.'
- Patterns define boundaries: Singleton for lifecycle management, Observer for event propagation, Strategy for algorithm selection.
- Python's dynamic nature means you can often implement patterns with fewer lines than in statically typed languages, but the trade-off is less compile-time safety.
Singleton Pattern — The One You Think You Know
Singleton ensures a class has only one instance and provides a global point of access. In Python, this seems trivial: just create a module-level object. But the devil is in the details. A thread-safe Singleton requires careful initialisation, and the classic double-checked locking pattern is tricky to get right because of Python's GIL and object ordering.
Let's break down the production-grade Singleton. The key is that __new__ is called every time you instantiate the class — even if the instance already exists. The common mistake is to put the instance check in __init__ instead of __new__, which means the constructor still runs on every call, potentially resetting state.
A better approach for most Python applications is to use a module-level function with @lru_cache. This gives you a thread-safe, lazily initialised singleton without locks. But it comes with a caveat: you cannot easily clear the cache for testing without accessing the wrapped function's cache clear method. Use this for objects that truly live for the entire process lifetime, like a configuration object that never changes after startup.
get_settings directly cannot be reused in a different context. Always ask: does this need to be unique per entire process, or just per request/thread?__new__ is slower than lru_cache by about 15% due to method overhead.lru_cache, but not for __new__.Factory Pattern — Object Creation Without the Pain
The Factory pattern separates object creation from business logic. In Python, this is often as simple as a function that returns different types based on input. The GoF Factory Method and Abstract Factory patterns translate naturally: Python's dynamic typing and duck typing mean you don't need complex interface hierarchies. A Factory can be a single function or a class hierarchy with a common interface.
The real power of Factory in Python comes with configuration-driven creation. You can map string keys to classes and instantiate them at runtime. This is how many frameworks (like Celery or Django's backends) work. But watch out for circular imports: if the factory function imports all possible classes, you may introduce a circular dependency. A pattern that works well is to use a lazy registry: class definitions register themselves in a dict at import time, and the factory only needs to know the dict.
Performance note: Factory dispatch via a dict is O(1) and preferable to a long if/elif chain. Use functools.singledispatch for function overloading based on argument type, but avoid using it when you need string-based dispatch for configuration.
typing.Protocol with structural subtyping (duck typing) allows you to define the interface without requiring inheritance. This decouples the factory from the concrete classes. No need to import all subclasses upfront.KeyError due to a typo in the configuration key.Observer Pattern — Event-Driven Without the Memory Leak
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. Python has several built-in mechanisms for this: property setters can trigger callbacks, and the blinker library or PyPubSub provide publish-subscribe. But the classic GoF Observer often leads to memory leaks in Python because observers hold strong references to subjects, or vice versa.
In Python, the biggest gotcha is the __del__ method. If a subject holds a strong reference to an observer that goes out of scope normally, the observer is not garbage collected because the subject still has a reference. This can cause unbounded memory growth, especially in long-running processes like web servers.
The fix: use weakref.ref for the observer references. Python's WeakSet and WeakValueDictionary are perfect for this. But you can't use weak references for bound methods easily — a bound method holds a strong reference to its object, so even a weak reference to the method won't help. Instead, store the actual object and call the method via weak reference, or use the weakref module's finalize for cleanup.
Another approach: use callbacks as simple functions (not methods) stored in a set of weak references to functions. But functions can be closures that capture large objects — so be mindful of what the closure captures.
- The subject keeps a weak reference to the observer — the subject doesn't prolong the observer's lifetime.
- When the observer is garbage collected, the weak reference returns
None. The subject cleans up dead refs during the next notification. - This pattern is essential for preventing memory leaks in event-driven systems with dynamic subscription.
self (the method's object), creating a reference cycle that weak references can't break.weakref.finalize to auto-unregister when the observer is garbage collected.Strategy Pattern — Replace if/elif Chains with Dispatch
Strategy lets you define a family of algorithms, encapsulate each one, and make them interchangeable. In Python, you don't need a formal interface; you can use a dictionary of callables. This is often simpler and more readable than a class hierarchy. The Strategy pattern shines when you have multiple ways to process the same input, like different compression algorithms or payment gateways.
Performance consideration: if the strategy selection logic is based on a configuration string that rarely changes, use a functools.lru_cache on the strategy selection function to avoid repeated parsing. If the strategies themselves have different performance characteristics, profile them independently and choose based on input characteristics (e.g., small data use compression-X, large data use compression-Y).
The biggest mistake is putting the selection logic in the caller's code, scattered across the codebase. Centralise the dispatch in a dedicated strategy registry so that adding a new strategy doesn't require changes in multiple places.
isinstance on every item in a data pipeline.Decorator Pattern — Python's Superpower and Its Pitfalls
Python has a built-in syntax for the Decorator pattern: the @decorator syntax. It's one of the few languages where the GoF Decorator pattern is directly supported. But with great power comes great responsibility. Decorators can change the signature of the decorated function, break type hints, and introduce subtle performance overhead.
The three most common production issues with decorators: 1. Signature loss: The decorated function loses its original __name__, __doc__, and argument signature. Always use @functools.wraps. 2. Performance overhead: Every call to a decorated function adds the decorator's overhead. If you chain several decorators, each one adds a function call. For latency-critical paths, consider combining decorators or using a single decorator that does multiple things. 3. State leakage: If a decorator has mutable state (e.g., a cache), concurrent access can cause data races. Use thread-safe structures or limit to single-threaded contexts.
The GoF Decorator pattern (wrapping objects) is also useful in Python: e.g., wrapping a file object to add compression or encryption. Use io.IOBase as the base class and implement the wrapper, or simply use functools.partial for simple cases.
@functools.wraps on the inner async function. Otherwise, the decorated async def becomes a regular function that returns a coroutine, but calling it twice creates two different coroutines — a common bug.time.perf_counter and reconsider the chain for hot paths.@functools.wraps to preserve function metadata.The Singleton That Silently Burned Through Database Connections
class MyPool: with a class variable, but the import was done inside a conditional block that ran on every request due to Python's module caching nuances. In Python, module-level objects are created once, but if the module was re-imported dynamically (e.g., via importlib.reload during hot-reload), the Singleton was recreated. Additionally, multiple threads called the factory function without a lock, causing race conditions that created temporary duplicate pools before the second one was discarded, but the connections remained open._pool = None and a thread-safe factory function using threading.Lock. Used atexit to register clean shutdown of the pool. Also made the connection pool a module attribute imported by all services, not passed as a dependency.- A Singleton is only as singleton as the module import system allows — watch out for dynamic reloads or multiple interpreters.
- Thread-safety in Singleton creation isn't optional in Python; always use a lock or
functools.lru_cachefor thread-safe caching. - Dependency injection beats Singleton for testability. Use Singleton only for truly stateless or globally unique resources like connection pools with cleanup.
id(pool) to confirm it's the same object.weakref.WeakSet to avoid duplicates but ensure callbacks are properly unregistered.raise ValueError(f"Unknown type: {type_name}").isinstance checks. Use a dictionary dispatch instead. Profile with time.perf_counter() before and after the selection.functools.wraps to preserve metadata. For decorators that change the return type, ensure the wrapped function still reports correct annotations. Check inspect.signature in tests.@functools.lru_cache(maxsize=1) on the factory function.Key takeaways
lru_cache for thread safety and simplicity; avoid class-based Singletons unless you need reset capability.self.@functools.wraps; measure overhead on hot paths; prefer composition for object-level decoration.Common mistakes to avoid
5 patternsUsing Singleton as a global variable for state that should be scoped (e.g., per request)
g object or dependency injection) instead of Singleton. Reserve Singleton for truly global resources like connection pools or configuration.Implementing Factory with a long if/elif chain instead of a dictionary dispatch
Not using weak references in Observer pattern, causing memory leaks in long-running processes
weakref.ref or WeakSet. Ensure callbacks are top-level functions or have short lifecycles. Use weakref.finalize for automatic unregistration.Forgetting `@functools.wraps` in decorators, breaking introspection and libraries that use function signatures
help(decorated_func) shows generic wrapper info. Unit tests that inspect signatures break.@functools.wraps(func) to the inner wrapper function. For async decorators, use @functools.wraps on the inner async function.Using Strategy pattern with class hierarchy when a simple dict would suffice
Interview Questions on This Topic
Explain how you would implement a thread-safe Singleton in Python. What are the trade-offs between different approaches?
@functools.lru_cache(maxsize=1). This is thread-safe because lru_cache locks internally (via the GIL for the cache but also via a real lock in CPython for the wrapper). Alternative: use a class with __new__ and double-checked locking with threading.Lock. The lru_cache approach is cleaner and faster for most cases, but it's harder to clear the cache in tests; you need to call get_settings.cache_clear(). The class-based approach allows more control over lifecycle (e.g., resetting the instance on config change) but risks race conditions if not implemented correctly. Real production choice: use lru_cache for configuration objects that never change after startup; use class-based with lock for objects that need manual reset.Frequently Asked Questions
That's Advanced Python. Mark it forged?
6 min read · try the examples if you haven't