Mid-level 6 min · March 06, 2026

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.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
Plain-English First

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.

io/thecodeforge/patterns/singleton.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from io.thecodeforge.utils import get_config

# Thread-safe Singleton using module-level cache
class DatabasePool:
    _instance = None
    _lock = None

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            import threading
            if cls._lock is None:
                cls._lock = threading.Lock()
            with cls._lock:
                if cls._instance is None:
                    cls._instance = cls(get_config())
        return cls._instance

    def __init__(self, config):
        self.conn = create_connection(config)

# Better approach: use module-level singleton with lru_cache
from functools import lru_cache

@lru_cache(maxsize=1)
def get_db_pool():
    return create_connection(get_config())
Output
Both approaches yield a single instance. The `lru_cache` version is simpler but cannot be easily cleared for testing. The class-based double-checked locking is production-ready for high-concurrency scenarios.
Mental Model: Patterns as Vocabulary
  • 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.
Production Insight
A team that blindly applies GOF patterns without considering Python's module system ends up with code that's harder to debug.
The module-level Singleton is the default Pythonic approach, but it breaks when you need to mock the instance in tests.
Rule: Prefer dependency injection for testability; use module-level Singletons only for truly global resources like connection pools with proper cleanup.
Key Takeaway
Design patterns in Python are tools, not rules.
Adapt the GoF patterns to Python's idioms: module-level cache, function decorators, and dynamic dispatch.
The pattern isn't the implementation — it's the intent behind the structure.
When to Use Which Pattern in Python
IfNeed exactly one instance of a resource, and you control the lifecycle
UseUse a module-level Singleton with a lock or lru_cache.
IfObject creation logic is complex or configurable
UseUse Factory Method or Abstract Factory. In Python, a simple function or classmethod suffices.
IfMultiple objects need to be notified of state changes without tight coupling
UseUse Observer with weak references to avoid memory leaks.
IfNeed to swap algorithms at runtime based on context
UseUse Strategy pattern via a dictionary of callables or a class hierarchy.
IfNeed to add behaviour to existing objects without modifying their class
UseUse Decorator (built-in via function decorators) or the GoF Decorator pattern via wrapper classes.

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.

io/thecodeforge/patterns/singleton_prod.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
from functools import lru_cache
from io.thecodeforge.config import Settings

@lru_cache(maxsize=1)
def get_settings() -> Settings:
    """Production-grade singleton for application settings.
    Thread-safe, lazy-loaded, and easy to use in decorators."""
    return Settings.from_env()

# Usage anywhere in the codebase, no import-time instantiation
# The lru_cache ensures only one Settings object ever exists per process.
Output
Testing: you can patch `get_settings` to return a mock. But you can also call `get_settings.cache_clear()` in a test teardown, though that's fragile if other tests depend on it. Prefer dependency injection for testable code.
Warning: Singleton != Global Variable
A Singleton is not a substitute for a global variable. It should have a defined purpose and lifecycle. Using Singletons for logging or configuration is common, but be aware that it creates hidden dependencies. A function that imports 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?
Production Insight
The classic Singleton with __new__ is slower than lru_cache by about 15% due to method overhead.
The real production risk is not performance but leaky abstraction: Singletons make unit tests interdependent.
Rule: If you use a Singleton, ensure it can be replaced in tests via a factory function or dependency injection.
Key Takeaway
Lru_cache Singleton > class-based Singleton for most Python apps.
Thread safety is handled by the GIL for lru_cache, but not for __new__.
Never use a Singleton as a global state container — you'll regret it during refactoring.

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.

io/thecodeforge/patterns/factory.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from typing import Protocol
from io.thecodeforge.notifications import EmailSender, SMSSender, PushSender

class NotificationSender(Protocol):
    def send(self, message: str) -> bool: ...

class SenderFactory:
    _registry: dict[str, type[NotificationSender]] = {}

    @classmethod
    def register(cls, sender_type: str, sender_cls: type[NotificationSender]):
        cls._registry[sender_type] = sender_cls

    @classmethod
    def create(cls, sender_type: str, **kwargs) -> NotificationSender:
        if sender_type not in cls._registry:
            raise ValueError(f"Unknown sender type: {sender_type}")
        return cls._registry[sender_type](**kwargs)

# Registration:
SenderFactory.register('email', EmailSender)
SenderFactory.register('sms', SMSSender)

# Usage:
sender = SenderFactory.create('email', config=settings.email_config)
sender.send('Hello')
Output
The registry pattern allows extensibility without modifying the factory. New senders can be added from other modules or even third-party packages via entry points.
Pythonic Factory: Use Protocols, Not ABCs
Python's 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.
Production Insight
The most common production failure with Factory is a KeyError due to a typo in the configuration key.
Always validate the registry entries at startup by instantiating each type with minimal arguments.
Rule: Add a test that iterates over all registered types and calls a basic method to catch import errors early.
Key Takeaway
Factory in Python = a dict of classes or a single dispatch function.
Use a registry pattern for extensibility; avoid monolithic if/elif chains.
Register types at import time to keep the factory independent of concrete classes.

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.

io/thecodeforge/patterns/observer.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import weakref
from typing import Callable, Set

class Observable:
    def __init__(self):
        self._observers: Set[weakref.ref] = set()

    def register(self, callback: Callable):
        self._observers.add(weakref.ref(callback))

    def unregister(self, callback: Callable):
        # Use a temporary set to collect dead refs
        dead = set()
        for ref in self._observers:
            obj = ref()
            if obj is None:
                dead.add(ref)
            elif obj is callback:
                dead.add(ref)
        self._observers -= dead

    def notify(self, *args, **kwargs):
        dead = set()
        for ref in self._observers:
            callback = ref()
            if callback:
                callback(*args, **kwargs)
            else:
                dead.add(ref)
        self._observers -= dead

# Usage:
def on_data_change(data):
    print(f"Data changed: {data}")

obs = Observable()
obs.register(on_data_change)
# The function `on_data_change` can be garbage collected when no longer needed
Output
This implementation automatically removes dead references during iteration. But it still relies on the callback being a top-level function or a callable that doesn't hold a strong reference to its parent object.
Mental Model: Weak References as Leash
  • 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.
Production Insight
A common production bug: registering a lambda inside a method as an observer. The lambda captures self (the method's object), creating a reference cycle that weak references can't break.
Rule: only register top-level functions or callable objects that have a short lifecycle.
Use weakref.finalize to auto-unregister when the observer is garbage collected.
Key Takeaway
Observer without weak references = memory leak in production.
Weak reference to the observer, not the callback, is the cleanest pattern.
Never use lambda or bound method directly as an observer callback in long-lived subjects.

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.

io/thecodeforge/patterns/strategy.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from io.thecodeforge.payment import CreditCardPay, PayPalPay, CryptoPay

# Strategy registry as a dict of callables
_payment_strategies = {
    'credit_card': CreditCardPay,
    'paypal': PayPalPay,
    'crypto': CryptoPay,
}

def get_payment_strategy(method: str):
    """Return the payment strategy class or callable."""
    strategy = _payment_strategies.get(method)
    if strategy is None:
        raise ValueError(f"Unsupported payment method: {method}")
    return strategy

# Usage:
strategy_cls = get_payment_strategy(order.payment_method)
processor = strategy_cls(config)
processor.charge(order.amount)
Output
Each strategy is a class with a `charge` method. The registry is a simple dict. Adding a new payment method means adding a new class and registering it — no changes to the rest of the code.
Strategy vs Factory — Don't Confuse Them
Factory creates objects; Strategy selects an algorithm. A Factory may use Strategy to decide which object to create, but they're different patterns. If you find yourself passing different parameters to the same class, that's probably Strategy. If you're selecting different classes based on configuration, that's Factory.
Production Insight
The biggest performance trap is evaluating a strategy's applicability inside a hot loop — e.g., checking isinstance on every item in a data pipeline.
Solution: pre-select the strategy once outside the loop.
Rule: keep strategy selection logic outside the critical path by caching the selected strategy.
Key Takeaway
Strategy = dictionary dispatch, not class hierarchy.
Pre-select the strategy once to avoid repeated cost.
Test each strategy independently with its own data characteristics.

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.

io/thecodeforge/decorators/logging.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import functools
import logging
from typing import Callable

logger = logging.getLogger(__name__)

def logged(func: Callable) -> Callable:
    """Logs entry and exit of the decorated function.
    Preserves signature via wraps."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logger.debug("Calling %s with args=%s kwargs=%s", func.__name__, args, kwargs)
        try:
            result = func(*args, **kwargs)
            logger.debug("%s returned %s", func.__name__, result)
            return result
        except Exception as e:
            logger.exception("%s raised %s", func.__name__, e)
            raise
    return wrapper

# Usage:
@logged
def process_order(order_id: int) -> bool:
    ...
# process_order.__name__ is still 'process_order', not 'wrapper'
Output
The `@functools.wraps` copies `__name__`, `__doc__`, `__module__`, and `__annotations__` from the original function to the wrapper. This is essential for tools that inspect function signatures, like FastAPI's route handlers.
Warning: Decorators and Async Functions
If you write a decorator that wraps an async function, you must return a coroutine wrapper and use @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.
Production Insight
A production incident: a chain of three decorators (logging, caching, rate-limiting) added 1.5ms overhead to a 10ms API call — a 15% increase. The team moved caching before logging to reduce log volume and kept only one decorator per critical path.
Rule: measure decorator overhead with time.perf_counter and reconsider the chain for hot paths.
Key Takeaway
Always use @functools.wraps to preserve function metadata.
Decorator overhead is additive — chain wisely.
For object-level decoration, prefer composition over inheritance (GoF Decorator style).
● Production incidentPOST-MORTEMseverity: high

The Singleton That Silently Burned Through Database Connections

Symptom
After deploying a new microservice that also used the shared Singleton pool, the number of open connections jumped to 3x the configured max. Only the original service was supposed to use the pool.
Assumption
The Singleton pattern was implemented using a module-level variable, which the team assumed was thread-safe and would prevent multiple instances of the connection pool.
Root cause
The Singleton implementation used 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.
Fix
Replaced the class-based Singleton with a module-level _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.
Key lesson
  • 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_cache for thread-safe caching.
  • Dependency injection beats Singleton for testability. Use Singleton only for truly stateless or globally unique resources like connection pools with cleanup.
Production debug guideSymptom → Action patterns for the five most common design pattern issues in Python microservices.5 entries
Symptom · 01
Singleton connection pool keeps opening new connections
Fix
Check if the Singleton is being re-created due to dynamic imports or multiple threads. Add a unique ID to the instance and log it on creation. Use id(pool) to confirm it's the same object.
Symptom · 02
Observer callbacks fire multiple times for one event
Fix
Check if callbacks are being registered multiple times (e.g., in a loop that runs on every request). Log the number of subscribers before and after registration. Use weakref.WeakSet to avoid duplicates but ensure callbacks are properly unregistered.
Symptom · 03
Factory method returns wrong object type
Fix
Verify that the registry mapping (e.g., dict of string to class) is correctly populated. Check for spelling in config keys. Add a fallback that raises a clear error: raise ValueError(f"Unknown type: {type_name}").
Symptom · 04
Strategy pattern executes slow branch even when input is fast
Fix
Check if the strategy selection logic itself is O(n) — e.g., evaluating all strategies with isinstance checks. Use a dictionary dispatch instead. Profile with time.perf_counter() before and after the selection.
Symptom · 05
Decorator causes function signature loss and breaks dependency injection
Fix
Use 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.
★ Quick Debug Cheat Sheet for Python Design PatternsUse these commands when a pattern-related bug surfaces in production. Each row shows symptom, immediate action, first diagnostic command, second command, and the fix.
Singleton not returning same instance
Immediate action
Log `id(instance)` and a stack trace to see how many times the factory was called.
Commands
python -c "from io.thecodeforge.db import pool; print(id(pool))"
grep -rn "import.*pool" /app | head -20
Fix now
Replace class-based Singleton with @functools.lru_cache(maxsize=1) on the factory function.
Observer memory leak (app grows RAM indefinitely)+
Immediate action
Take a heap dump with `pympler` or `objgraph` to see which objects are holding references.
Commands
python -m objgraph -al /tmp/snapshot.pkl
print(len(subject._observers)) in a memory profiling endpoint
Fix now
Switch to weakref.ref for observer references and ensure callbacks are properly unregistered in __del__ or context managers.
Factory returns object that fails at runtime+
Immediate action
Check the type of the returned object and compare to expected interface.
Commands
python -c "from io.thecodeforge.factory import create; obj = create('config'); print(type(obj).__name__)"
grep -rn "config" /app/registry.py
Fix now
Add a unit test that verifies every registry entry returns an object with the expected methods.
Decorated function loses docstring or signature+
Immediate action
Inspect the function's `__doc__` and `__signature__`.
Commands
python -c "from io.thecodeforge.decorators import logged; print(logged(my_func).__doc__)"
inspect.signature(logged(my_func))
Fix now
Wrap the decorator with @functools.wraps(func) and use functools.singledispatch if you need overloaded decorators.
Pattern Comparison at a Glance
PatternIntentPython IdiomCommon GotchaPerformance Note
SingletonEnsure one instanceModule-level function with lru_cacheThread safety in __new__lru_cache is ~15% faster than class-based
FactoryDecouple object creationRegistry dict with ProtocolCircular imports on registrationDict dispatch is O(1)
ObserverOne-to-many notificationWeakSet of callbacksMemory leak due to strong referencesWeak reference cleanup adds O(n) overhead during notify
StrategyInterchangeable algorithmsDict of callablesSelecting strategy inside hot loopCache selected strategy for performance
DecoratorAdd behaviour dynamicallyFunction decorators or wrapper classesSignature loss without wrapsOverhead is ~0.5µs per decorator per call

Key takeaways

1
Design patterns are vocabulary for architecture, not rigid templates
adapt them to Python's idioms.
2
Singleton
use lru_cache for thread safety and simplicity; avoid class-based Singletons unless you need reset capability.
3
Factory
registry dict with Protocol beats if/elif chains and trivially extends.
4
Observer
always use weak references to avoid memory leaks; never register bound methods or lambdas that capture self.
5
Strategy
dict dispatch over class hierarchy; select once outside hot loops.
6
Decorator
always @functools.wraps; measure overhead on hot paths; prefer composition for object-level decoration.
7
Patterns are tools, not goals
if a pattern makes your code harder to understand, you're using it wrong.
8
Test every pattern implementation
Singleton concurrency, Factory registry completeness, Observer cleanup, Strategy performance, Decorator signature.

Common mistakes to avoid

5 patterns
×

Using Singleton as a global variable for state that should be scoped (e.g., per request)

Symptom
Two different requests share the same mutable state, causing data leakage between users. For example, a Singleton 'current user' object that is set at the start of a request but not reset.
Fix
Use request-scoped dependencies (e.g., Flask's 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

Symptom
Adding a new type requires modifying the factory function, violating the Open/Closed principle. The function becomes hard to maintain and test.
Fix
Use a registry dict that maps type names to classes. New types register themselves at import time. The factory becomes a single dictionary lookup.
×

Not using weak references in Observer pattern, causing memory leaks in long-running processes

Symptom
Memory usage grows over time; heap analysis shows many instances of observer objects that should have been garbage collected.
Fix
Store observer references using 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

Symptom
FastAPI routes fail with validation errors, or help(decorated_func) shows generic wrapper info. Unit tests that inspect signatures break.
Fix
Always apply @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

Symptom
Codebase has many strategy classes with only one method each, and the selection logic is scattered across the codebase.
Fix
Use a dictionary mapping condition to callable (like a function or lambda). If the strategies share some logic, use a base class, but avoid the full GoF overhead until the complexity justifies it.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how you would implement a thread-safe Singleton in Python. What ...
Q02SENIOR
How would you implement the Observer pattern in Python to avoid memory l...
Q03SENIOR
What is the difference between the Factory Method and Abstract Factory p...
Q04SENIOR
When should you avoid using the Singleton pattern entirely?
Q01 of 04SENIOR

Explain how you would implement a thread-safe Singleton in Python. What are the trade-offs between different approaches?

ANSWER
The simplest thread-safe Singleton uses a module-level variable with @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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the simplest way to implement Singleton in Python?
02
How do I avoid memory leaks with the Observer pattern in Python?
03
What's the difference between Strategy and Factory patterns?
04
Why does `@functools.wraps` matter in Python decorators?
05
Can I use these design patterns in other languages like Java or JS?
🔥

That's Advanced Python. Mark it forged?

6 min read · try the examples if you haven't

Previous
Abstract Base Classes in Python
13 / 17 · Advanced Python
Next
Python Performance Optimisation