🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
Search courses…
💼 Servicesℹ️ About✉️ ContactView Pricing Plansfrom $10

Design Patterns: Singleton, Factory, Observer, Strategy in Python

PythonDesign Patterns⭐ Premium

Advertisement

Google, Meta & Amazon Interview

Design Patterns: Singleton, Factory, Observer, Strategy in Python

Implementing GoF design patterns in Pythonic ways

Interview Question

"Implement the Singleton, Factory, Observer, and Strategy patterns in Python. How do you make them thread-safe? When would you use each pattern?"

Difficulty: Medium | Frequently asked at Google, Meta, Amazon


Theoretical Foundation

What are Design Patterns?

Design patterns are reusable solutions to common software design problems. They're not code but templates for solving specific issues.

# Design patterns categorized:
# 1. Creational: How objects are created (Singleton, Factory)
# 2. Structural: How objects are composed (Adapter, Decorator)
# 3. Behavioral: How objects communicate (Observer, Strategy)

# Python advantage: Many patterns are simpler due to dynamic typing
# and first-class functions

ℹ️

Key Concept: Design patterns are guidelines, not rules. Use them when they solve a real problem.


Singleton Pattern

Problem

Ensure a class has only one instance and provide global access to it.

Implementation

import threading
from typing import Optional

# Method 1: Using __new__
class Singleton:
    _instance: Optional['Singleton'] = None
    _lock = threading.Lock()
    
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance
    
    def __init__(self, value=None):
        if not hasattr(self, '_initialized'):
            self.value = value
            self._initialized = True

# Method 2: Using metaclass
class SingletonMeta(type):
    _instances = {}
    _lock = threading.Lock()
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            with cls._lock:
                if cls not in cls._instances:
                    instance = super().__call__(*args, **kwargs)
                    cls._instances[cls] = instance
        return cls._instances[cls]

class Database(metaclass=SingletonMeta):
    def __init__(self, connection_string: str):
        self.connection_string = connection_string
        self.connected = False
    
    def connect(self):
        self.connected = True
        print(f"Connected to {self.connection_string}")

# Method 3: Using decorator
def singleton(cls):
    instances = {}
    lock = threading.Lock()
    
    def get_instance(*args, **kwargs):
        if cls not in instances:
            with lock:
                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, message):
        self.logs.append(message)
        print(f"LOG: {message}")

# Usage
db1 = Database("postgresql://localhost/mydb")
db2 = Database("postgresql://localhost/mydb")  # Same instance

print(f"Same instance: {db1 is db2}")
db1.connect()
print(f"DB2 connected: {db2.connected}")

Output:

Architecture Diagram
Same instance: True
Connected to postgresql://localhost/mydb
DB2 connected: True

Thread-Safe Singleton

import threading
import time

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()
    
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance
    
    def __init__(self):
        if not hasattr(self, 'initialized'):
            self.initialized = True
            self.data = {}
            print("Singleton initialized")

def create_singleton(thread_id):
    """Create singleton from multiple threads."""
    singleton = ThreadSafeSingleton()
    print(f"Thread {thread_id}: {id(singleton)}")

# Test thread safety
threads = []
for i in range(5):
    t = threading.Thread(target=create_singleton, args=(i,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

Output:

Architecture Diagram
Singleton initialized
Thread 0: 140234866577152
Thread 1: 140234866577152
Thread 2: 140234866577152
Thread 3: 140234866577152
Thread 4: 140234866577152

⚠️

Pythonic Alternative: Consider using module-level variables instead of Singleton. Python modules are naturally singletons.


Factory Pattern

Problem

Create objects without specifying their exact class.

Implementation

from abc import ABC, abstractmethod
from typing import Dict, Type

# Product interface
class Animal(ABC):
    @abstractmethod
    def speak(self) -> str:
        pass
    
    @abstractmethod
    def move(self) -> str:
        pass

# Concrete products
class Dog(Animal):
    def speak(self) -> str:
        return "Woof!"
    
    def move(self) -> str:
        return "Runs on four legs"

class Cat(Animal):
    def speak(self) -> str:
        return "Meow!"
    
    def move(self) -> str:
        return "Slinks gracefully"

class Bird(Animal):
    def speak(self) -> str:
        return "Tweet!"
    
    def move(self) -> str:
        return "Flies in the sky"

# Simple Factory
class AnimalFactory:
    _animals: Dict[str, Type[Animal]] = {
        "dog": Dog,
        "cat": Cat,
        "bird": Bird,
    }
    
    @classmethod
    def create(cls, animal_type: str) -> Animal:
        animal_class = cls._animals.get(animal_type.lower())
        if not animal_class:
            raise ValueError(f"Unknown animal type: {animal_type}")
        return animal_class()
    
    @classmethod
    def register(cls, animal_type: str, animal_class: Type[Animal]):
        cls._animals[animal_type.lower()] = animal_class

# Usage
animals = ["dog", "cat", "bird"]
for animal_type in animals:
    animal = AnimalFactory.create(animal_type)
    print(f"{animal_type.title()}: {animal.speak()} {animal.move()}")

Output:

Architecture Diagram
Dog: Woof! Runs on four legs
Cat: Meow! Slinks gracefully
Bird: Tweet! Flies in the sky

Abstract Factory

from abc import ABC, abstractmethod

# Abstract products
class Button(ABC):
    @abstractmethod
    def render(self) -> str:
        pass

class Checkbox(ABC):
    @abstractmethod
    def render(self) -> str:
        pass

# Concrete products
class WindowsButton(Button):
    def render(self) -> str:
        return "Windows Button"

class WindowsCheckbox(Checkbox):
    def render(self) -> str:
        return "Windows Checkbox"

class MacButton(Button):
    def render(self) -> str:
        return "Mac Button"

class MacCheckbox(Checkbox):
    def render(self) -> str:
        return "Mac Checkbox"

# Abstract factory
class GUIFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button:
        pass
    
    @abstractmethod
    def create_checkbox(self) -> Checkbox:
        pass

# Concrete factories
class WindowsFactory(GUIFactory):
    def create_button(self) -> Button:
        return WindowsButton()
    
    def create_checkbox(self) -> Checkbox:
        return WindowsCheckbox()

class MacFactory(GUIFactory):
    def create_button(self) -> Button:
        return MacButton()
    
    def create_checkbox(self) -> Checkbox:
        return MacCheckbox()

# Client code
class Application:
    def __init__(self, factory: GUIFactory):
        self.factory = factory
        self.button = None
        self.checkbox = None
    
    def create_ui(self):
        self.button = self.factory.create_button()
        self.checkbox = self.factory.create_checkbox()
    
    def render(self):
        print(f"Button: {self.button.render()}")
        print(f"Checkbox: {self.checkbox.render()}")

# Usage
import platform

if platform.system() == "Windows":
    factory = WindowsFactory()
else:
    factory = MacFactory()

app = Application(factory)
app.create_ui()
app.render()

💡

Interview Tip: Factory patterns are useful when you need to create objects based on configuration or runtime conditions.


Observer Pattern

Problem

Define a one-to-many dependency between objects so that when one object changes state, all dependents are notified.

Implementation

from typing import List, Callable, Any
from dataclasses import dataclass, field
from datetime import datetime

# Event system
@dataclass
class Event:
    name: str
    data: Any
    timestamp: datetime = field(default_factory=datetime.now)

class EventEmitter:
    """Simple event emitter for Observer pattern."""
    
    def __init__(self):
        self._listeners: dict[str, List[Callable]] = {}
    
    def on(self, event_name: str, callback: Callable):
        """Register event listener."""
        if event_name not in self._listeners:
            self._listeners[event_name] = []
        self._listeners[event_name].append(callback)
    
    def off(self, event_name: str, callback: Callable):
        """Remove event listener."""
        if event_name in self._listeners:
            self._listeners[event_name].remove(callback)
    
    def emit(self, event_name: str, data: Any = None):
        """Emit event to all listeners."""
        event = Event(name=event_name, data=data)
        
        if event_name in self._listeners:
            for callback in self._listeners[event_name]:
                callback(event)

# Concrete subjects
class User(EventEmitter):
    def __init__(self, name: str):
        super().__init__()
        self.name = name
        self._email = ""
    
    @property
    def email(self):
        return self._email
    
    @email.setter
    def email(self, value: str):
        old_email = self._email
        self._email = value
        self.emit("email_changed", {
            "user": self.name,
            "old_email": old_email,
            "new_email": value
        })

# Observers
def log_email_change(event: Event):
    print(f"[LOG] Email changed: {event.data}")

def send_notification(event: Event):
    print(f"[NOTIFY] Sending notification to {event.data['new_email']}")

def update_database(event: Event):
    print(f"[DB] Updating database for {event.data['user']}")

# Usage
user = User("Alice")

# Register observers
user.on("email_changed", log_email_change)
user.on("email_changed", send_notification)
user.on("email_changed", update_database)

# Change email (triggers all observers)
user.email = "alice@example.com"

Output:

Architecture Diagram
[LOG] Email changed: {'user': 'Alice', 'old_email': '', 'new_email': 'alice@example.com'}
[NOTIFY] Sending notification to alice@example.com
[DB] Updating database for Alice

Advanced Observer with Weak References

import weakref
from typing import List, Any, Callable

class WeakObserver:
    """Observer that uses weak references to avoid memory leaks."""
    
    def __init__(self):
        self._listeners: dict[str, List[weakref.WeakMethod]] = {}
    
    def on(self, event_name: str, callback: Callable):
        """Register weak event listener."""
        if event_name not in self._listeners:
            self._listeners[event_name] = []
        
        # Use WeakMethod for bound methods
        try:
            weak_ref = weakref.WeakMethod(callback)
        except TypeError:
            # For regular functions
            weak_ref = weakref.ref(callback)
        
        self._listeners[event_name].append(weak_ref)
    
    def emit(self, event_name: str, data: Any = None):
        """Emit event to all live listeners."""
        if event_name not in self._listeners:
            return
        
        # Clean up dead references
        self._listeners[event_name] = [
            ref for ref in self._listeners[event_name]
            if ref() is not None
        ]
        
        for weak_ref in self._listeners[event_name]:
            callback = weak_ref()
            if callback is not None:
                callback(event_name, data)

# Usage
class User:
    def __init__(self, name):
        self.name = name
        self.emitter = WeakObserver()
    
    def set_email(self, email):
        self.emitter.emit("email_changed", {"user": self.name, "email": email})

def handler(event_name, data):
    print(f"Handled {event_name}: {data}")

user = User("Bob")
user.emitter.on("email_changed", handler)
user.set_email("bob@example.com")  # Works

del handler  # Handler can be garbage collected
user.set_email("bob2@example.com")  # No error

ℹ️

Memory Safety: Weak references prevent memory leaks when observers are deleted.


Strategy Pattern

Problem

Define a family of algorithms, encapsulate each one, and make them interchangeable.

Implementation

from abc import ABC, abstractmethod
from typing import List
from dataclasses import dataclass

# Strategy interface
class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data: List[int]) -> List[int]:
        pass
    
    @abstractmethod
    def name(self) -> str:
        pass

# Concrete strategies
class BubbleSort(SortStrategy):
    def sort(self, data: List[int]) -> List[int]:
        arr = data.copy()
        n = len(arr)
        for i in range(n):
            for j in range(0, n-i-1):
                if arr[j] > arr[j+1]:
                    arr[j], arr[j+1] = arr[j+1], arr[j]
        return arr
    
    def name(self) -> str:
        return "Bubble Sort"

class QuickSort(SortStrategy):
    def sort(self, data: List[int]) -> List[int]:
        if len(data) <= 1:
            return data
        pivot = data[len(data) // 2]
        left = [x for x in data if x < pivot]
        middle = [x for x in data if x == pivot]
        right = [x for x in data if x > pivot]
        return self.sort(left) + middle + self.sort(right)
    
    def name(self) -> str:
        return "Quick Sort"

class MergeSort(SortStrategy):
    def sort(self, data: List[int]) -> List[int]:
        if len(data) <= 1:
            return data
        mid = len(data) // 2
        left = self.sort(data[:mid])
        right = self.sort(data[mid:])
        return self._merge(left, right)
    
    def _merge(self, left: List[int], right: List[int]) -> List[int]:
        result = []
        i = j = 0
        while i < len(left) and j < len(right):
            if left[i] < right[j]:
                result.append(left[i])
                i += 1
            else:
                result.append(right[j])
                j += 1
        result.extend(left[i:])
        result.extend(right[j:])
        return result
    
    def name(self) -> str:
        return "Merge Sort"

# Context
class Sorter:
    def __init__(self, strategy: SortStrategy):
        self._strategy = strategy
    
    @property
    def strategy(self) -> SortStrategy:
        return self._strategy
    
    @strategy.setter
    def strategy(self, strategy: SortStrategy):
        self._strategy = strategy
    
    def sort(self, data: List[int]) -> List[int]:
        print(f"Sorting with {self._strategy.name()}")
        return self._strategy.sort(data)

# Usage
data = [64, 34, 25, 12, 22, 11, 90]

sorter = Sorter(BubbleSort())
print(f"Bubble: {sorter.sort(data)}")

sorter.strategy = QuickSort()
print(f"Quick: {sorter.sort(data)}")

sorter.strategy = MergeSort()
print(f"Merge: {sorter.sort(data)}")

Output:

Architecture Diagram
Sorting with Bubble Sort
Bubble: [11, 12, 22, 25, 34, 64, 90]
Sorting with Quick Sort
Quick: [11, 12, 22, 25, 34, 64, 90]
Sorting with Merge Sort
Merge: [11, 12, 22, 25, 34, 64, 90]

Strategy with Functions

from typing import Callable, List
from dataclasses import dataclass

# Strategies as functions
def discount_10_percent(price: float) -> float:
    return price * 0.9

def discount_20_percent(price: float) -> float:
    return price * 0.8

def free_shipping(price: float) -> float:
    return price - 5.99  # Assuming $5.99 shipping

# Context using functions
class ShoppingCart:
    def __init__(self):
        self.items: List[dict] = []
        self.discount_strategy: Callable[[float], float] = None
    
    def add_item(self, name: str, price: float):
        self.items.append({"name": name, "price": price})
    
    def set_discount(self, strategy: Callable[[float], float]):
        self.discount_strategy = strategy
    
    def total(self) -> float:
        total = sum(item["price"] for item in self.items)
        if self.discount_strategy:
            total = self.discount_strategy(total)
        return total

# Usage
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99)

print(f"No discount: ${cart.total():.2f}")

cart.set_discount(discount_10_percent)
print(f"10% off: ${cart.total():.2f}")

cart.set_discount(discount_20_percent)
print(f"20% off: ${cart.total():.2f}")

Output:

Architecture Diagram
No discount: $1029.98
10% off: $926.98
20% off: $823.98

💡

Interview Tip: Strategy pattern is great for algorithms that need to be swapped at runtime (sorting, pricing, validation).


Other Important Patterns

Decorator Pattern

from abc import ABC, abstractmethod

# Component interface
class Coffee(ABC):
    @abstractmethod
    def cost(self) -> float:
        pass
    
    @abstractmethod
    def description(self) -> str:
        pass

# Concrete component
class SimpleCoffee(Coffee):
    def cost(self) -> float:
        return 2.00
    
    def description(self) -> str:
        return "Simple coffee"

# Decorator base
class CoffeeDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        self._coffee = coffee
    
    def cost(self) -> float:
        return self._coffee.cost()
    
    def description(self) -> str:
        return self._coffee.description()

# Concrete decorators
class MilkDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.50
    
    def description(self) -> str:
        return f"{self._coffee.description()}, milk"

class SugarDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.25
    
    def description(self) -> str:
        return f"{self._coffee.description()}, sugar"

# Usage
coffee = SimpleCoffee()
print(f"{coffee.description()}: ${coffee.cost():.2f}")

coffee_with_milk = MilkDecorator(coffee)
print(f"{coffee_with_milk.description()}: ${coffee_with_milk.cost():.2f}")

coffee_with_milk_sugar = SugarDecorator(coffee_with_milk)
print(f"{coffee_with_milk_sugar.description()}: ${coffee_with_milk_sugar.cost():.2f}")

Output:

Architecture Diagram
Simple coffee: $2.00
Simple coffee, milk: $2.50
Simple coffee, milk, sugar: $2.75

Command Pattern

from abc import ABC, abstractmethod
from typing import List

# Command interface
class Command(ABC):
    @abstractmethod
    def execute(self):
        pass
    
    @abstractmethod
    def undo(self):
        pass

# Concrete commands
class TextEditor:
    def __init__(self):
        self.text = ""
    
    def insert(self, text: str):
        self.text += text
    
    def delete(self, length: int):
        self.text = self.text[:-length] if length else self.text

class InsertCommand(Command):
    def __init__(self, editor: TextEditor, text: str):
        self.editor = editor
        self.text = text
    
    def execute(self):
        self.editor.insert(self.text)
    
    def undo(self):
        self.editor.delete(len(self.text))

class DeleteCommand(Command):
    def __init__(self, editor: TextEditor, length: int):
        self.editor = editor
        self.length = length
        self.deleted_text = ""
    
    def execute(self):
        self.deleted_text = self.editor.text[-self.length:]
        self.editor.delete(self.length)
    
    def undo(self):
        self.editor.insert(self.deleted_text)

# Invoker
class CommandManager:
    def __init__(self):
        self.history: List[Command] = []
        self.undone: List[Command] = []
    
    def execute(self, command: Command):
        command.execute()
        self.history.append(command)
        self.undone.clear()
    
    def undo(self):
        if self.history:
            command = self.history.pop()
            command.undo()
            self.undone.append(command)
    
    def redo(self):
        if self.undone:
            command = self.undone.pop()
            command.execute()
            self.history.append(command)

# Usage
editor = TextEditor()
manager = CommandManager()

# Execute commands
manager.execute(InsertCommand(editor, "Hello"))
manager.execute(InsertCommand(editor, " World"))
print(f"After insert: '{editor.text}'")

manager.execute(DeleteCommand(editor, 5))
print(f"After delete: '{editor.text}'")

# Undo
manager.undo()
print(f"After undo: '{editor.text}'")

manager.undo()
print(f"After undo: '{editor.text}'")

Output:

Architecture Diagram
After insert: 'Hello World'
After delete: 'Hello'
After undo: 'Hello World'
After undo: 'Hello'

Complexity Analysis

Pattern Overhead

PatternTime ComplexitySpace ComplexityUse Case
SingletonO(1) accessO(1)Global state
FactoryO(1) creationO(n) classesObject creation
ObserverO(n) notificationO(n) listenersEvent systems
StrategyO(1) swapO(1)Algorithm selection

When to Use

# Singleton: Global state, resource management
# - Database connections
# - Configuration
# - Logging

# Factory: Object creation based on conditions
# - Plugin systems
# - Document parsers
# - UI components

# Observer: Event-driven systems
# - GUI events
# - Message queues
# - React systems

# Strategy: Algorithm selection
# - Sorting algorithms
# - Payment methods
# - Compression algorithms

Interview Tips

Common Follow-up Questions

  1. "When would you NOT use these patterns?"

    • Over-engineering simple solutions
    • When language features already solve the problem
    • When patterns add unnecessary complexity
  2. "How do you make Singleton thread-safe?"

    • Double-checked locking
    • Module-level variables (Pythonic)
    • threading.Lock
  3. "What's the difference between Factory and Abstract Factory?"

    • Factory: Creates one product type
    • Abstract Factory: Creates families of related products

Code Review Tips

# BAD: Singleton everywhere
class BadSingleton:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

# GOOD: Use module-level when possible
# config.py
_config = {"debug": False, "db_url": "..."}
def get_config():
    return _config

# BAD: Observer with memory leaks
class BadObserver:
    def __init__(self):
        self.listeners = []  # Strong references
    
    def add_listener(self, listener):
        self.listeners.append(listener)  # Never garbage collected!

# GOOD: Observer with weak references
import weakref
class GoodObserver:
    def __init__(self):
        self.listeners = []  # Weak references
    
    def add_listener(self, listener):
        self.listeners.append(weakref.ref(listener))

⚠️

Common Mistake: Overusing design patterns. They're tools, not goals. Use them only when they solve a real problem.


Summary

PatternCategoryPurposePython Implementation
SingletonCreationalSingle instance__new__, metaclass, module
FactoryCreationalObject creationFunctions, classes
ObserverBehavioralEvent notificationCallbacks, weakref
StrategyBehavioralAlgorithm selectionFunctions, classes
DecoratorStructuralAdd behaviorPython decorators
CommandBehavioralEncapsulate actionsClasses with execute/undo

Best Practices

  1. Use Pythonic alternatives when possible (module variables for Singleton)
  2. Keep patterns simple - don't over-engineer
  3. Document pattern usage clearly
  4. Test patterns independently
  5. Consider performance implications

ℹ️

Key Takeaway: Design patterns solve common problems. Use them judiciously and prefer Pythonic solutions when available.


Practice Problems

  1. Singleton Logger: Implement a thread-safe singleton logger
  2. Payment Factory: Create a factory for different payment methods
  3. Event System: Build an observer pattern for a chat application
  4. Compression Strategy: Implement different compression algorithms
  5. Undo/Redo: Build a command pattern with undo/redo functionality

Further Reading

  • Gang of Four Book: "Design Patterns: Elements of Reusable Object-Oriented Software"
  • Python Patterns: "Python Cookbook" by David Beazley
  • Refactoring Guru: https://refactoring.guru/design-patterns
  • Python Docs: abc module, weakref module

Remember: Design patterns are tools, not goals. Use them to solve real problems, not to show off.

Advertisement