Design Patterns
Master the essential design patterns used in professional Python development. Learn Singleton, Factory, and Strategy patterns with real-world examples and best practices.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
1. What Are Design Patterns (Really)?
Design patterns are reusable solutions to common design problems in software.
They're not code you copy 1:1. They're mental templates you adapt:
Problem You Have
Pattern to Use
Real Example
"I need only one of this thing globally"
Singleton
Database connection, Logger
"I need a flexible way to create objects"
Factory
Payment processors, Notification senders
"I need to swap behaviour/algorithm easily"
Strategy
Discount calculators, Sorting algorithms
- Not over-engineering (patterns are tools, not religion)
- Using Python features (modules, first-class functions, dataclasses, typing)
- Keeping code readable and testable
- Singleton – one global instance
- Factory – flexible object creation
- Strategy – swappable algorithms/behaviours
Part 1: Singleton Pattern
2. Singleton Pattern — Intent & When to Use
Intent: Ensure only one instance of a class exists, and provide a global access point to it.
- Configuration manager
- Database connection pool
- Logging manager
- Global cache
3. The Simplest Python Singleton: Just Use a Module
Instead of creating a complex class-based singleton, you can just create a module:
- config is imported once
- Python caches modules in sys.modules
- Every import config gets the same object
👉 For many apps, this is the most Pythonic "singleton".
Only use heavy OO-singleton patterns if you really need class-based behaviour.
4. Classic OO Singleton With __new__
- We override __new__ (object creation) not __init__ (initialisation).
- We store the one instance in cls._instance .
- We guard __init__ with a flag ( _initialised ) to avoid re-running logic.
- Looks like a normal class.
- Supports inheritance.
- Can be overkill for many cases.
- Harder to test (global state).
5. Singleton via Decorator (Reusable & Clean)
We can build a @singleton decorator that turns any class into a singleton:
- @singleton replaces the class with a function ( get_instance )
- Calling Logger() actually calls get_instance()
- The real instance is cached in instances[cls]
- Super simple to apply.
- Easy to reuse on different classes.
- You lose the "type" a bit — Logger is now actually a function returning a Logger instance (can confuse IDEs / type checkers).
- Some tools (like MyPy) might need extra hints.
6. The Borg Pattern (Shared State Singleton)
Changing an attribute in one changes it for all.
- When you want instances that act independent for type/identity, but share config or state.
- Some advanced patterns where identity and shared state are separate concerns.
7. Singleton in Real Projects — When & When NOT to Use
- Application-wide config (with care)
- Database connection pool / engine (like SQLAlchemy's engine)
- Global metrics collector
- Logging facility
- To "hack around" passing objects properly
- To avoid dependency injection
- To hide poor architecture or circular dependencies
- For everything — overuse = messy, untestable code
Example of something better than a singleton: dependency injection:
Part 2: Factory Patterns
Factory patterns control how objects are created :
- You want to be able to easily swap implementations
- You don't want if type == "x" everywhere in the code
- You want code that is open for extension, closed for modification
- Simple Factory (function-based)
- Factory Method (OO inheritance-based)
- Abstract Factory (families of objects)
8. Simple Factory (Function-Based)
This is the most Pythonic and often best starting point.
We want to create different notification senders: email, SMS, push.
- Your app code only knows about create_notifier("email")
- All the "which class?" logic is inside the factory function
If you add WhatsApp later, only one place changes.
9. Improving the Simple Factory with a Registry
Instead of if/elif, we can use a mapping (much cleaner for many types).
- Easy to extend: just add to NOTIFIER_REGISTRY
- Great for config-driven systems: channel can come from JSON/env
This "simple factory + registry" pattern is used everywhere in real-world Python code (ML frameworks, plugin systems, etc.).
10. Factory Method Pattern (OO Style)
Sometimes you don't want a separate function, you want subclasses to decide what they create.
Factory Method = a method in a parent class that is overridden in subclasses to decide what object to create.
- process_payment() is defined once in the base class
- create_client() is the factory method
- Each subclass defines what concrete client it wants
- You have a common workflow (template), but some steps vary
- You want subclasses to control those steps via factory methods
11. Abstract Factory — Creating "Families" of Related Objects
You're building a cross-platform GUI toolkit:
- On Windows: WindowsButton, WindowsCheckbox
- On macOS: MacButton, MacCheckbox
- Ensure you never accidentally mix WindowsButton with MacCheckbox
- Make it easy to "switch theme" or "switch platform" by changing one factory
Abstract Factory returns a family of related objects.
- You switch entire families by swapping the factory.
- You never accidentally pair incompatible components.
- Theme systems
- Cross-platform widgets
- Database driver families (e.g. Postgres/MySQL adapters)
Part 3: Strategy Pattern
The Strategy pattern allows you to change how something works without changing the code that uses it.
Python makes this pattern extremely powerful because functions are first-class objects.
12. Why Strategy Pattern Exists
- Hard to add new rules (must edit this function)
- Conditionals spread everywhere
- Testing each rule becomes harder
- Breaking Open–Closed Principle (OCP)
13. Strategy Pattern — Functional (Most Pythonic)
The simplest and BEST way in Python is using functions as strategies.
- ✔ most Pythonic
- ✔ extremely fast
- ✔ ideal for data pipelines, ML preprocessing, game logic, pricing engines
14. Strategy Using Classes (Classic OOP Approach)
Sometimes you need stateful strategies or polymorphism.
- When strategies need state
- When strategies become complex
- When building enterprise-level frameworks
15. Strategy in Real Systems
▶ 1. Machine Learning Preprocessing Pipelines
Choose pricing or fraud-checking algorithms at runtime.
- follow-player
- Reward calculation strategies
- Exploration strategies (epsilon-greedy, Boltzmann, UCB)
- write-through
- Black Friday discount mode
- Bulk discounts
- VIP customer strategies
🎓 Conclusion
You now fully understand all 3 major design patterns:
Control instance creation, global state, config, caching.
- Control which objects are created
- Promote extensibility
- Reduce conditionals
- Enable plugin architectures
- Control how objects behave
- Swap algorithms at runtime
- Enable configurability and clean architecture
- 🔥 scalable systems
- 🔥 cleaner abstraction
- 🔥 professional architecture
- 🔥 maintainable large codebases
- 🔥 flexible logic for real projects
📋 Quick Reference — Design Patterns
Pattern
Use when
Only one instance needed (config, DB pool)
Create objects without exposing class details
Observer
Event-driven: notify multiple subscribers
Swap algorithms at runtime
Decorator
Add behaviour without changing the class
You can now recognise and apply the most important design patterns — the vocabulary every senior engineer uses when discussing architecture.
Up next: Module Architecture — structure large Python codebases with clear boundaries and imports.
Practice quiz
Which problem does the Singleton pattern solve?
- Swapping algorithms at runtime
- Creating families of related objects
- Ensuring only one instance exists with a global access point
- Adding behavior to a class dynamically
Answer: Ensuring only one instance exists with a global access point. Singleton guarantees a single instance (e.g. a config or DB pool) and provides one global access point to it.
What is the most Pythonic 'singleton' for many simple cases?
- Just a module — imported once and cached in sys.modules
- A metaclass
- A global list
- A frozen dataclass
Answer: Just a module — imported once and cached in sys.modules. A module is imported once and cached in sys.modules, so every import gets the same object — a natural singleton.
In the classic singleton, which method is overridden to control instance creation?
- __init__
- __call__
- __enter__
- __new__
Answer: __new__. __new__ controls object creation, so it returns the stored single instance; __init__ only initializes.
What distinguishes the Borg pattern from a classic singleton?
- It forbids inheritance
- Many instances exist but they share the same state via __dict__
- It allows only one instance
- It caches return values
Answer: Many instances exist but they share the same state via __dict__. Borg instances are distinct objects (s1 is s2 is False) yet share one __dict__, so they share all state.
What does a simple (function-based) factory like create_notifier('email') achieve?
- It centralizes the 'which class?' decision so callers don't use if/elif everywhere
- It caches notifications
- It makes the class a singleton
- It validates argument types
Answer: It centralizes the 'which class?' decision so callers don't use if/elif everywhere. The factory hides class-selection logic in one place, so app code just asks for 'email' without knowing the concrete class.
How does a registry improve a simple factory?
- It adds threading
- It removes the need for classes
- It replaces if/elif chains with a dictionary mapping names to classes
- It enforces a single instance
Answer: It replaces if/elif chains with a dictionary mapping names to classes. A registry dict maps keys to classes, so adding a new type is just one dictionary entry instead of another elif branch.
In the Factory Method pattern, who decides which concrete object is created?
- A standalone function
- Subclasses, by overriding the factory method
- The caller passes the class in
- A global registry only
Answer: Subclasses, by overriding the factory method. Factory Method puts a create_*() method in the base class that each subclass overrides to return its own product.
What does an Abstract Factory return?
- A single object
- A cached value
- A function
- A family of related objects (e.g. matching Button and Checkbox)
Answer: A family of related objects (e.g. matching Button and Checkbox). Abstract Factory produces whole families of related objects, ensuring you never mix incompatible components.
What is the most Pythonic way to implement the Strategy pattern?
- A deep class hierarchy
- Functions as first-class strategies stored in a dict
- A singleton per strategy
- Global if/elif branches
Answer: Functions as first-class strategies stored in a dict. Because functions are first-class in Python, storing them in a strategy dict is the simplest, fastest approach.
Which principle does replacing if/elif algorithm-selection with Strategy uphold?
- DRY only
- Single instance guarantee
- The Open-Closed Principle (open for extension, closed for modification)
- Lazy initialization
Answer: The Open-Closed Principle (open for extension, closed for modification). Strategy lets you add new behaviors without editing existing selection code, honoring the Open-Closed Principle.