Courses/Python/Design Patterns in Python

    Lesson 31 β€’ Advanced

    Design Patterns in Python (Singleton, Factory, Strategy)

    Master the essential design patterns used in professional Python development. Learn Singleton, Factory, and Strategy patterns with real-world examples and best practices.

    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 HavePattern to UseReal Example
    "I need only one of this thing globally"SingletonDatabase connection, Logger
    "I need a flexible way to create objects"FactoryPayment processors, Notification senders
    "I need to swap behaviour/algorithm easily"StrategyDiscount calculators, Sorting algorithms

    In Python, we also care about:

    • Not over-engineering (patterns are tools, not religion)
    • Using Python features (modules, first-class functions, dataclasses, typing)
    • Keeping code readable and testable

    In this lesson we'll cover:

    1. Singleton – one global instance
    2. Factory – flexible object creation
    3. 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.

    Typical use cases:

    • 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:

    Module as Singleton

    The simplest Pythonic singleton pattern

    Try it Yourself Β»
    Python
    # config.py
    
    SETTINGS = {
        "debug": True,
        "db_url": "postgres://localhost",
    }
    
    def is_debug():
        return SETTINGS["debug"]
    
    # Then everywhere:
    # anywhere.py
    # import config
    # print(config.SETTINGS["db_url"])
    
    print(SETTINGS)
    print(is_debug())
    • 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__

    Let's look at the "canonical" pattern:

    Classic Singleton with __new__

    The canonical OO singleton pattern

    Try it Yourself Β»
    Python
    class Singleton:
        _instance = None
    
        def __new__(cls, *args, **kwargs):
            if cls._instance is None:
                # create the one and only instance
                cls._instance = super().__new__(cls)
            return cls._instance
    
    class AppConfig(Singleton):
        def __init__(self, debug=False):
            # __init__ may be called multiple times, be careful
            if not hasattr(self, "_initialised"):
                self.debug = debug
                self._initialised = True
    
    config1 = AppConfig(de
    ...

    Key points:

    • 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.

    Pros:

    • Looks like a normal class.
    • Supports inheritance.

    Cons:

    • 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 Decorator

    A reusable decorator to make any class a singleton

    Try it Yourself Β»
    Python
    def singleton(cls):
        instances = {}
    
        def get_instance(*args, **kwargs):
            if cls not in instances:
                instances[cls] = cls(*args, **kwargs)
            return instances[cls]
    
        return get_instance
    
    @singleton
    class Logger:
        def __init__(self):
            self.logs = []
    
        def log(self, msg):
            self.logs.append(msg)
    
    logger1 = Logger()
    logger2 = Logger()
    
    logger1.log("Hello")
    logger2.log("World")
    
    print(logger1 is logger2)  # True
    print(logger1.logs)        # ['Hello',
    ...

    What happened?

    • @singleton replaces the class with a function (get_instance)
    • Calling Logger() actually calls get_instance()
    • The real instance is cached in instances[cls]

    Pros:

    • Super simple to apply.
    • Easy to reuse on different classes.

    Cons:

    • 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)

    Alternative: many instances, shared state.

    Borg Pattern

    Shared state across multiple instances

    Try it Yourself Β»
    Python
    class Borg:
        _shared_state = {}
    
        def __init__(self):
            self.__dict__ = self._shared_state
    
    class Settings(Borg):
        def __init__(self):
            super().__init__()
            if not hasattr(self, "initialised"):
                self.debug = True
                self.initialised = True
    
    s1 = Settings()
    s2 = Settings()
    
    s1.debug = False
    print(s2.debug)        # False
    print(s1 is s2)        # False (different instances)

    All instances share the same __dict__.

    Changing an attribute in one changes it for all.

    When might this be useful?

    • 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

    βœ… Good scenarios:

    • Application-wide config (with care)
    • Database connection pool / engine (like SQLAlchemy's engine)
    • Global metrics collector
    • Logging facility

    ❌ Bad scenarios:

    • 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:

    class Service:
        def __init__(self, config):
            self.config = config
    
    service = Service(config=AppConfig(debug=True))
    
    # Easier to test:
    test_service = Service(config=FakeConfig())

    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

    We'll cover:

    1. Simple Factory (function-based)
    2. Factory Method (OO inheritance-based)
    3. Abstract Factory (families of objects)

    8. Simple Factory (Function-Based)

    This is the most Pythonic and often best starting point.

    Example: Notification Sender Factory

    We want to create different notification senders: email, SMS, push.

    Simple Factory

    Function-based factory for creating notification senders

    Try it Yourself Β»
    Python
    class EmailSender:
        def send(self, to, message):
            print(f"[EMAIL to {to}] {message}")
    
    class SMSSender:
        def send(self, to, message):
            print(f"[SMS to {to}] {message}")
    
    class PushSender:
        def send(self, to, message):
            print(f"[PUSH to {to}] {message}")
    
    def create_notifier(channel: str):
        channel = channel.lower()
        if channel == "email":
            return EmailSender()
        elif channel == "sms":
            return SMSSender()
        elif channel == "push":
            retu
    ...

    What changed?

    • 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).

    Factory with Registry

    Use a dictionary registry for cleaner factory pattern

    Try it Yourself Β»
    Python
    class EmailSender:
        def send(self, to, message):
            print(f"[EMAIL to {to}] {message}")
    
    class SMSSender:
        def send(self, to, message):
            print(f"[SMS to {to}] {message}")
    
    class PushSender:
        def send(self, to, message):
            print(f"[PUSH to {to}] {message}")
    
    NOTIFIER_REGISTRY = {
        "email": EmailSender,
        "sms": SMSSender,
        "push": PushSender,
    }
    
    def create_notifier(channel: str):
        try:
            cls = NOTIFIER_REGISTRY[channel.lower()]
        except KeyError:
        
    ...

    Benefits:

    • 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.

    Definition:

    Factory Method = a method in a parent class that is overridden in subclasses to decide what object to create.

    Factory Method Pattern

    Subclasses decide which objects to create

    Try it Yourself Β»
    Python
    from abc import ABC, abstractmethod
    
    class PaymentProcessor(ABC):
        @abstractmethod
        def create_client(self):
            """Factory method: subclasses return appropriate client."""
            pass
    
        def process_payment(self, amount: float):
            client = self.create_client()
            client.charge(amount)
    
    class StripeClient:
        def charge(self, amount):
            print(f"[Stripe] Charging Β£{amount:.2f}")
    
    class PayPalClient:
        def charge(self, amount):
            print(f"[PayPal] Charging Β£{a
    ...

    Here:

    • process_payment() is defined once in the base class
    • create_client() is the factory method
    • Each subclass defines what concrete client it wants

    This pattern is great when:

    • 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

    Problem:

    You're building a cross-platform GUI toolkit:

    • On Windows: WindowsButton, WindowsCheckbox
    • On macOS: MacButton, MacCheckbox

    You want to:

    • 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.

    Abstract Factory

    Create families of related objects

    Try it Yourself Β»
    Python
    from abc import ABC, abstractmethod
    
    # Product interfaces
    class Button(ABC):
        @abstractmethod
        def render(self): ...
    
    class Checkbox(ABC):
        @abstractmethod
        def render(self): ...
    
    # Concrete products
    class LightButton(Button):
        def render(self):
            print("[Light Button]")
    
    class DarkButton(Button):
        def render(self):
            print("[Dark Button]")
    
    class LightCheckbox(Checkbox):
        def render(self):
            print("[Light Checkbox]")
    
    class DarkCheckbox(Checkbox):
        def 
    ...

    Key benefits:

    • You switch entire families by swapping the factory.
    • You never accidentally pair incompatible components.

    This is similar to:

    • 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.

    In simple terms:

    Strategy = a set of interchangeable algorithms you can swap at runtime.

    Python makes this pattern extremely powerful because functions are first-class objects.

    12. Why Strategy Pattern Exists

    Without Strategy, developers often write:

    if method == "percentage":
        total = price * 0.9
    elif method == "fixed":
        total = price - 5
    elif method == "bogo":
        total = handle_bogo(price)

    Problems:

    • Hard to add new rules (must edit this function)
    • Conditionals spread everywhere
    • Testing each rule becomes harder
    • Breaking Open–Closed Principle (OCP)

    Strategy solves all this.

    13. Strategy Pattern β€” Functional (Most Pythonic)

    The simplest and BEST way in Python is using functions as strategies.

    Functional Strategy Pattern

    The most Pythonic way to implement strategies

    Try it Yourself Β»
    Python
    # Step 1: Define behaviors
    def discount_percentage(price):
        return price * 0.9
    
    def discount_fixed(price):
        return price - 5
    
    def discount_bogo(price):
        return price / 2
    
    # Step 2: Strategy dictionary
    DISCOUNT_STRATEGIES = {
        "percentage": discount_percentage,
        "fixed": discount_fixed,
        "bogo": discount_bogo
    }
    
    # Step 3: Apply strategy
    def apply_discount(price, strategy_name):
        strategy = DISCOUNT_STRATEGIES[strategy_name]
        return strategy(price)
    
    print(apply_discount(100
    ...

    Benefits:

    • βœ” easiest
    • βœ” 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.

    Class-Based Strategy

    OOP approach for stateful strategies

    Try it Yourself Β»
    Python
    from abc import ABC, abstractmethod
    
    # Step 1: Strategy interface
    class DiscountStrategy(ABC):
        @abstractmethod
        def apply(self, price):
            pass
    
    # Step 2: Implement concrete strategies
    class PercentageDiscount(DiscountStrategy):
        def apply(self, price):
            return price * 0.9
    
    class FixedDiscount(DiscountStrategy):
        def apply(self, price):
            return price - 5
    
    # Step 3: Use strategy
    def calculate_total(price, strategy: DiscountStrategy):
        return strategy.apply(price
    ...

    Why use this?

    • When strategies need state
    • When strategies become complex
    • When building enterprise-level frameworks

    15. Strategy in Real Systems

    β–Ά 1. Machine Learning Preprocessing Pipelines

    strategies = {
        "normalize": lambda x: (x - x.mean()) / x.std(),
        "standardize": lambda x: x / 255,
        "none": lambda x: x,
    }

    β–Ά 2. Payment Processing Logic

    Choose pricing or fraud-checking algorithms at runtime.

    β–Ά 3. Game Development

    Enemy movement strategies:

    • aggressive
    • defensive
    • random
    • follow-player

    β–Ά 4. AI Agents

    • Reward calculation strategies
    • Exploration strategies (epsilon-greedy, Boltzmann, UCB)

    β–Ά 5. Web Framework Routing

    Different caching strategies:

    • LRU
    • LFU
    • write-through
    • write-back

    β–Ά 6. E-commerce Pricing

    • Black Friday discount mode
    • Bulk discounts
    • VIP customer strategies

    πŸŽ“ Conclusion

    You now fully understand all 3 major design patterns:

    βœ” Singleton

    Control instance creation, global state, config, caching.

    βœ” Factory (Simple, Method, Abstract)

    • Control which objects are created
    • Promote extensibility
    • Reduce conditionals
    • Enable plugin architectures

    βœ” Strategy

    • Control how objects behave
    • Swap algorithms at runtime
    • Enable configurability and clean architecture

    These patterns together give you:

    • πŸ”₯ scalable systems
    • πŸ”₯ cleaner abstraction
    • πŸ”₯ professional architecture
    • πŸ”₯ maintainable large codebases
    • πŸ”₯ flexible logic for real projects

    πŸ“‹ Quick Reference β€” Design Patterns

    PatternUse when
    SingletonOnly one instance needed (config, DB pool)
    FactoryCreate objects without exposing class details
    ObserverEvent-driven: notify multiple subscribers
    StrategySwap algorithms at runtime
    DecoratorAdd behaviour without changing the class

    πŸŽ‰ Great work! You've completed this lesson.

    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.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy Policy β€’ Terms of Service