πŸŽ‰ 75% of content is free forever β€” Unlock Premium from $10/mo β†’
CW
Search courses…
πŸ’Ό Servicesℹ️ Aboutβœ‰οΈ ContactView Pricing Plansfrom $10

Design Patterns in Python

Python InterviewSoftware Architecture⭐ Premium

Advertisement

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

  1. When would you use the Factory pattern over direct object instantiation?

  2. How do you implement thread-safe Singleton in Python?

  3. Explain the difference between Observer and Pub-Sub patterns.

  4. When is the Strategy pattern more appropriate than inheritance?

  5. How do you handle command serialization in the Command pattern?

Advertisement