Python Patterns Guide
Objective explanations, simple examples, and a [Link] call for each pattern.
Quick rule of thumb
Use Strategy when behavior changes, Factory when creation changes, Adapter when an external API does not fit
your interface, Repository for persistence abstraction, Producer-Consumer for queues and async workloads,
Dependency Injection for testability, Ports and Adapters for clean architecture, and DDD when the business domain
should drive the model.
Strategy
Objective: Same contract, different behavior. Use it when you want to switch algorithms or execution logic without
changing the caller.
Example
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> str:
...
class PixPayment(PaymentStrategy):
def pay(self, amount: float) -> str:
return f"Paid {amount} with PIX"
class CreditCardPayment(PaymentStrategy):
def pay(self, amount: float) -> str:
return f"Paid {amount} with Credit Card"
class Checkout:
def __init__(self, strategy: PaymentStrategy) -> None:
[Link] = strategy
def finish(self, amount: float) -> str:
return [Link](amount)
[Link] call
from strategy_example import Checkout, PixPayment, CreditCardPayment
def main():
checkout = Checkout(PixPayment())
print([Link](150.0))
checkout = Checkout(CreditCardPayment())
print([Link](150.0))
if __name__ == "__main__":
main()
Factory
Objective: Centralizes object creation. Use it when the caller should not know which concrete class is being
instantiated.
Example
class EmailNotifier:
def send(self, message: str) -> str:
return f"Email sent: {message}"
class SmsNotifier:
def send(self, message: str) -> str:
return f"SMS sent: {message}"
class NotifierFactory:
@staticmethod
def create(channel: str):
if channel == "email":
return EmailNotifier()
if channel == "sms":
return SmsNotifier()
raise ValueError("Unsupported channel")
[Link] call
from factory_example import NotifierFactory
def main():
notifier = [Link]("email")
print([Link]("Hello"))
if __name__ == "__main__":
main()
Adapter
Objective: Wraps an incompatible interface and exposes the interface your app expects.
Example
class ExternalRedisClient:
def rpush(self, queue_name: str, value: str) -> None:
print(f"Redis RPUSH {queue_name}: {value}")
class QueueAdapter:
def __init__(self, redis_client: ExternalRedisClient, queue_name: str) -> None:
self.redis_client = redis_client
self.queue_name = queue_name
def enqueue(self, message: str) -> None:
self.redis_client.rpush(self.queue_name, message)
[Link] call
from adapter_example import ExternalRedisClient, QueueAdapter
def main():
redis_client = ExternalRedisClient()
queue = QueueAdapter(redis_client, "events")
[Link]("device-online")
if __name__ == "__main__":
main()
Repository
Objective: Separates persistence from business logic. Your domain code talks to a repository, not directly to SQL or
an ORM session.
Example
class Device:
def __init__(self, device_id: str, status: str) -> None:
self.device_id = device_id
[Link] = status
class DeviceRepository:
def __init__(self) -> None:
self._storage: dict[str, Device] = {}
def save(self, device: Device) -> None:
self._storage[device.device_id] = device
def get_by_id(self, device_id: str) -> Device | None:
return self._storage.get(device_id)
[Link] call
from repository_example import Device, DeviceRepository
def main():
repo = DeviceRepository()
[Link](Device("dev-1", "online"))
device = repo.get_by_id("dev-1")
print([Link] if device else "not found")
if __name__ == "__main__":
main()
Observer / Pub-Sub
Objective: One event notifies multiple listeners. Good for alerts, logs, dashboards, and loosely coupled reactions.
Example
class EventBus:
def __init__(self) -> None:
self._subscribers: dict[str, list] = {}
def subscribe(self, event_name: str, callback) -> None:
self._subscribers.setdefault(event_name, []).append(callback)
def publish(self, event_name: str, payload: dict) -> None:
for callback in self._subscribers.get(event_name, []):
callback(payload)
def log_listener(payload: dict) -> None:
print("LOG:", payload)
def alert_listener(payload: dict) -> None:
print("ALERT:", payload)
[Link] call
from observer_example import EventBus, log_listener, alert_listener
def main():
bus = EventBus()
[Link]("[Link]", log_listener)
[Link]("[Link]", alert_listener)
[Link]("[Link]", {"device_id": "dev-1", "temp": 87})
if __name__ == "__main__":
main()
Producer-Consumer
Objective: Producers create work quickly; consumers process it later. Great for decoupling MQTT/API ingestion
from slower processing.
Example
import asyncio
queue: [Link][dict] = [Link]()
async def producer() -> None:
await [Link]({"device_id": "dev-1", "temp": 80})
async def worker() -> None:
while True:
message = await [Link]()
print("Processing", message)
queue.task_done()
[Link] call
import asyncio
from producer_consumer_example import producer, worker, queue
async def main():
worker_task = asyncio.create_task(worker())
await producer()
await [Link]()
worker_task.cancel()
if __name__ == "__main__":
[Link](main())
Dependency Injection
Objective: Pass dependencies from outside instead of creating them inside the class. This improves testing and
flexibility.
Example
class EmailService:
def send(self, message: str) -> None:
print("EMAIL:", message)
class NotificationManager:
def __init__(self, email_service: EmailService) -> None:
self.email_service = email_service
def notify(self, message: str) -> None:
self.email_service.send(message)
[Link] call
from di_example import EmailService, NotificationManager
def main():
email_service = EmailService()
manager = NotificationManager(email_service)
[Link]("Welcome")
if __name__ == "__main__":
main()
Ports and Adapters (Hexagonal)
Objective: Core code depends on abstractions (ports). Infrastructure implements them (adapters).
Example
from abc import ABC, abstractmethod
class MessageQueuePort(ABC):
@abstractmethod
async def enqueue(self, message: dict) -> None:
...
class InMemoryQueueAdapter(MessageQueuePort):
def __init__(self) -> None:
[Link]: list[dict] = []
async def enqueue(self, message: dict) -> None:
[Link](message)
class PublishTelemetryUseCase:
def __init__(self, queue: MessageQueuePort) -> None:
[Link] = queue
async def execute(self, payload: dict) -> None:
await [Link](payload)
[Link] call
import asyncio
from ports_adapters_example import InMemoryQueueAdapter, PublishTelemetryUseCase
async def main():
queue = InMemoryQueueAdapter()
use_case = PublishTelemetryUseCase(queue)
await use_case.execute({"device_id": "dev-1", "temp": 42})
print([Link])
if __name__ == "__main__":
[Link](main())
DDD (Domain-Driven Design) basics
Objective: Model the software around business concepts. Keep rules in the domain, orchestration in use cases,
persistence in repositories.
Example
class Device:
def __init__(self, device_id: str, max_temp: float) -> None:
self.device_id = device_id
self.max_temp = max_temp
def is_overheating(self, current_temp: float) -> bool:
return current_temp > self.max_temp
class DeviceRepository:
def __init__(self) -> None:
self._devices: dict[str, Device] = {
"dev-1": Device("dev-1", 75.0)
}
def get_by_id(self, device_id: str) -> Device | None:
return self._devices.get(device_id)
class AlertService:
def send(self, device_id: str, current_temp: float) -> None:
print(f"ALERT: {device_id} at {current_temp}")
class ProcessTelemetryUseCase:
def __init__(self, repo: DeviceRepository, alert_service: AlertService) -> None:
[Link] = repo
self.alert_service = alert_service
def execute(self, device_id: str, current_temp: float) -> None:
device = [Link].get_by_id(device_id)
if device and device.is_overheating(current_temp):
self.alert_service.send(device_id, current_temp)
[Link] call
from ddd_example import DeviceRepository, AlertService, ProcessTelemetryUseCase
def main():
repo = DeviceRepository()
alert_service = AlertService()
use_case = ProcessTelemetryUseCase(repo, alert_service)
use_case.execute("dev-1", 81.0)
if __name__ == "__main__":
main()
How to study these patterns
1. Learn the problem each pattern solves. 2. Recognize the tradeoff. 3. Practice small examples. 4. Map patterns to
real projects. 5. In interviews, explain why you chose the pattern, not just the name.
Interview tip
A strong answer is: 'I used Strategy because I had the same contract with different execution behaviors' or 'I used
Repository to keep SQL out of my domain logic.' That is better than naming patterns without context.