Design Patterns in Python
Difficulty: Medium-Hard | Companies: Google, Meta, Amazon, Netflix, Stripe
Creational Patterns
Singleton Pattern
from typing import Any, Optional
import threading
class SingletonMeta(type):
"""Thread-safe Singleton metaclass."""
_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):
"""Singleton database connection."""
def __init__(self):
self.connection = None
self._connect()
def _connect(self):
print("Establishing database connection...")
self.connection = "Connected"
def query(self, sql: str):
return f"Executing: {sql}"
# Alternative: Decorator-based Singleton
def singleton(cls):
"""Singleton decorator."""
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:
"""Singleton logger."""
def __init__(self):
self.logs = []
def log(self, message: str):
self.logs.append(message)
print(f"LOG: {message}")
# Usage
db1 = Database()
db2 = Database()
print(db1 is db2) # True
logger1 = Logger()
logger2 = Logger()
print(logger1 is logger2) # True
βΉοΈ
In Python, the module-level singleton is often preferred: create a single instance in a module and import it everywhere.
Factory Pattern
from abc import ABC, abstractmethod
from typing import Dict, Type, Any
class Notification(ABC):
"""Abstract notification class."""
@abstractmethod
def send(self, message: str) -> bool:
pass
@abstractmethod
def get_type(self) -> str:
pass
class EmailNotification(Notification):
def __init__(self, smtp_server: str, port: int):
self.smtp_server = smtp_server
self.port = port
def send(self, message: str) -> bool:
print(f"Email via {self.smtp_server}:{self.port}: {message}")
return True
def get_type(self) -> str:
return "email"
class SMSNotification(Notification):
def __init__(self, api_key: str):
self.api_key = api_key
def send(self, message: str) -> bool:
print(f"SMS via API: {message}")
return True
def get_type(self) -> str:
return "sms"
class PushNotification(Notification):
def __init__(self, device_token: str):
self.device_token = device_token
def send(self, message: str) -> bool:
print(f"Push to {self.device_token}: {message}")
return True
def get_type(self) -> str:
return "push"
class NotificationFactory:
"""Factory for creating notifications."""
_registry: Dict[str, Type[Notification]] = {}
@classmethod
def register(cls, notification_type: str, notification_class: Type[Notification]):
cls._registry[notification_type] = notification_class
@classmethod
def create(cls, notification_type: str, **kwargs) -> Notification:
if notification_type not in cls._registry:
raise ValueError(f"Unknown notification type: {notification_type}")
return cls._registry[notification_type](**kwargs)
@classmethod
def list_types(cls):
return list(cls._registry.keys())
# Register notification types
NotificationFactory.register("email", EmailNotification)
NotificationFactory.register("sms", SMSNotification)
NotificationFactory.register("push", PushNotification)
# Usage
email = NotificationFactory.create("email", smtp_server="smtp.gmail.com", port=587)
email.send("Hello via email!")
Structural Patterns
Adapter Pattern
from typing import Any, Dict
import json
class EuropeanSocket:
"""European power socket."""
def voltage(self) -> int:
return 230
def live(self) -> int:
return 1
def neutral(self) -> int:
return -1
def ground(self) -> int:
return 0
class AmericanSocket:
"""American power socket."""
def voltage(self) -> int:
return 120
def live(self) -> int:
return 1
def neutral(self) -> int:
return -1
class Adapter:
"""Adapter to make European device work with American socket."""
def __init__(self, socket: AmericanSocket):
self.socket = socket
def voltage(self) -> int:
return self.socket.voltage()
def live(self) -> int:
return self.socket.live()
def neutral(self) -> int:
return self.socket.neutral()
def ground(self) -> int:
return 0 # Adapter provides ground
class EuropeanDevice:
"""Device requiring European voltage."""
def __init__(self, socket):
self.socket = socket
def power_on(self):
if self.socket.voltage() > 200:
print("Device powered on safely")
else:
print("Voltage too low!")
# Usage
american_socket = AmericanSocket()
adapter = Adapter(american_socket)
device = EuropeanDevice(adapter)
device.power_on() # Works with adapter
# Data format adapter
class XMLData:
"""Simulated XML data source."""
def __init__(self, data: Dict):
self.data = data
def to_xml(self) -> str:
xml = "<data>"
for key, value in self.data.items():
xml += f"<{key}>{value}</{key}>"
xml += "</data>"
return xml
class JSONAdapter:
"""Adapter to convert XML interface to JSON."""
def __init__(self, xml_data: XMLData):
self.xml_data = xml_data
def to_json(self) -> str:
# Convert XML to JSON
xml_str = self.xml_data.to_xml()
# Simple conversion (in real code, use proper XML parser)
return json.dumps(self.xml_data.data)
# Usage
xml_data = XMLData({"name": "John", "age": 30})
json_adapter = JSONAdapter(xml_data)
print(json_adapter.to_json())
β οΈ
Use adapters when integrating third-party libraries or legacy systems that don't match your expected interface.
Behavioral Patterns
Strategy Pattern
from abc import ABC, abstractmethod
from typing import List, Callable
from functools import reduce
class SortStrategy(ABC):
"""Abstract sorting strategy."""
@abstractmethod
def sort(self, data: List) -> List:
pass
@abstractmethod
def get_name(self) -> str:
pass
class BubbleSort(SortStrategy):
def sort(self, data: List) -> List:
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 get_name(self) -> str:
return "Bubble Sort"
class QuickSort(SortStrategy):
def sort(self, data: List) -> List:
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 get_name(self) -> str:
return "Quick Sort"
class MergeSort(SortStrategy):
def sort(self, data: List) -> List:
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, right: List) -> List:
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 get_name(self) -> str:
return "Merge Sort"
class Sorter:
"""Context class for sorting strategies."""
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy):
self._strategy = strategy
def sort(self, data: List) -> List:
print(f"Sorting with {self._strategy.get_name()}")
return self._strategy.sort(data)
# Usage
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(QuickSort())
print(sorter.sort(data))
sorter.set_strategy(MergeSort())
print(sorter.sort(data))
Command Pattern
from abc import ABC, abstractmethod
from typing import List, Any
from datetime import datetime
class Command(ABC):
"""Abstract command interface."""
@abstractmethod
def execute(self) -> Any:
pass
@abstractmethod
def undo(self) -> Any:
pass
@abstractmethod
def description(self) -> str:
pass
class TextDocument:
"""Text document receiver."""
def __init__(self):
self.content = ""
self.cursor_position = 0
def insert(self, text: str, position: int):
self.content = self.content[:position] + text + self.content[position:]
self.cursor_position = position + len(text)
def delete(self, position: int, length: int) -> str:
deleted = self.content[position:position + length]
self.content = self.content[:position] + self.content[position + length:]
self.cursor_position = position
return deleted
def replace(self, position: int, length: int, new_text: str) -> str:
deleted = self.content[position:position + length]
self.content = self.content[:position] + new_text + self.content[position + length:]
self.cursor_position = position + len(new_text)
return deleted
class InsertTextCommand(Command):
"""Insert text command."""
def __init__(self, document: TextDocument, text: str, position: int):
self.document = document
self.text = text
self.position = position
self.timestamp = datetime.now()
def execute(self):
self.document.insert(self.text, self.position)
def undo(self):
self.document.delete(self.position, len(self.text))
def description(self):
return f"Insert '{self.text}' at position {self.position}"
class DeleteTextCommand(Command):
"""Delete text command."""
def __init__(self, document: TextDocument, position: int, length: int):
self.document = document
self.position = position
self.length = length
self.deleted_text = ""
self.timestamp = datetime.now()
def execute(self):
self.deleted_text = self.document.delete(self.position, self.length)
def undo(self):
self.document.insert(self.deleted_text, self.position)
def description(self):
return f"Delete {self.length} chars at position {self.position}"
class CommandHistory:
"""Manages command history with undo/redo."""
def __init__(self):
self._undo_stack: List[Command] = []
self._redo_stack: List[Command] = []
def execute(self, command: Command):
command.execute()
self._undo_stack.append(command)
self._redo_stack.clear()
print(f"Executed: {command.description()}")
def undo(self) -> bool:
if not self._undo_stack:
return False
command = self._undo_stack.pop()
command.undo()
self._redo_stack.append(command)
print(f"Undone: {command.description()}")
return True
def redo(self) -> bool:
if not self._redo_stack:
return False
command = self._redo_stack.pop()
command.execute()
self._undo_stack.append(command)
print(f"Redone: {command.description()}")
return True
def get_history(self) -> List[str]:
return [cmd.description() for cmd in self._undo_stack]
# Usage
doc = TextDocument()
history = CommandHistory()
history.execute(InsertTextCommand(doc, "Hello", 0))
history.execute(InsertTextCommand(doc, " World", 5))
print(doc.content) # Hello World
history.undo()
print(doc.content) # Hello
history.redo()
print(doc.content) # Hello World
βΉοΈ
Command pattern is essential for undo/redo functionality, transaction systems, and macro recording.
Observer Pattern
from typing import Callable, Dict, List, Any
from dataclasses import dataclass
from datetime import datetime
import weakref
@dataclass
class Event:
"""Event data class."""
name: str
data: Any
timestamp: datetime
source: str
class EventEmitter:
"""Event emitter with weak references."""
def __init__(self):
self._listeners: Dict[str, List] = {}
def on(self, event: str, callback: Callable) -> Callable:
"""Register event listener."""
if event not in self._listeners:
self._listeners[event] = []
# Use weak reference to prevent memory leaks
ref = weakref.WeakMethod(callback, self._on_ref_deleted(event))
self._listeners[event].append(ref)
def unsubscribe():
if event in self._listeners:
self._listeners[event] = [
r for r in self._listeners[event]
if r() is not None and r() != callback
]
return unsubscribe
def _on_ref_deleted(self, event: str) -> Callable:
def callback(ref):
if event in self._listeners:
self._listeners[event] = [
r for r in self._listeners[event] if r is not ref
]
return callback
def emit(self, event: str, data: Any = None, source: str = ""):
"""Emit event to all listeners."""
if event not in self._listeners:
return
event_data = Event(
name=event,
data=data,
timestamp=datetime.now(),
source=source
)
dead_refs = []
for ref in self._listeners[event]:
callback = ref()
if callback is not None:
callback(event_data)
else:
dead_refs.append(ref)
# Cleanup dead references
for ref in dead_refs:
self._listeners[event].remove(ref)
def once(self, event: str, callback: Callable) -> Callable:
"""Register one-time listener."""
def wrapper(event_data):
callback(event_data)
unsubscribe()
unsubscribe = self.on(event, wrapper)
return unsubscribe
# Usage
emitter = EventEmitter()
def on_user_created(event: Event):
print(f"User created: {event.data}")
def send_welcome_email(event: Event):
print(f"Sending email to {event.data.get('email')}")
emitter.on("user_created", on_user_created)
emitter.on("user_created", send_welcome_email)
emitter.emit("user_created", {"name": "John", "email": "john@example.com"})
Follow-Up Questions
-
When would you use the Factory pattern over direct object instantiation?
-
How do you implement thread-safe Singleton in Python?
-
Explain the difference between Observer and Pub-Sub patterns.
-
When is the Strategy pattern more appropriate than inheritance?
-
How do you handle command serialization in the Command pattern?