Python Patterns Skill
Instructions for AI
Apply idiomatic Python patterns, modern best practices, and production-grade code standards. Use this skill when writing, reviewing, or refactoring Python code.
Reference: atstaeff/better-python for concrete before/after examples of all patterns below.
Core Patterns
1. Repository Pattern
Abstraction over data access, enabling testability and swappable backends.
from typing import Protocol
from uuid import UUID
class Repository[T](Protocol):
async def get(self, id: UUID) -> T | None: ...
async def save(self, entity: T) -> None: ...
async def delete(self, id: UUID) -> None: ...
async def list_all(self) -> list[T]: ...
2. Service Layer Pattern
Business logic encapsulated in services with injected dependencies.
class OrderService:
def __init__(self, repo: OrderRepository, events: EventBus) -> None:
self._repo = repo
self._events = events
async def place_order(self, command: PlaceOrderCommand) -> Order:
order = Order.create(command)
await self._repo.save(order)
await self._events.publish(OrderPlaced(order_id=order.id))
return order
3. Domain Events
Decouple side effects from business logic.
from dataclasses import dataclass
from datetime import datetime
from uuid import UUID
@dataclass(frozen=True)
class DomainEvent:
occurred_at: datetime = field(default_factory=datetime.utcnow)
@dataclass(frozen=True)
class OrderPlaced(DomainEvent):
order_id: UUID
customer_id: UUID
total_amount: float
4. Result Pattern (Error Handling)
Explicit success/failure without exceptions for expected cases.
from dataclasses import dataclass
from typing import Generic, TypeVar
T = TypeVar("T")
E = TypeVar("E")
@dataclass(frozen=True)
class Ok(Generic[T]):
value: T
@dataclass(frozen=True)
class Err(Generic[E]):
error: E
type Result[T, E] = Ok[T] | Err[E]
# Usage
def divide(a: float, b: float) -> Result[float, str]:
if b == 0:
return Err("Division by zero")
return Ok(a / b)
5. Configuration with Pydantic Settings
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
database_url: str
redis_url: str = "redis://localhost:6379"
debug: bool = False
log_level: str = "INFO"
model_config = {"env_file": ".env", "env_prefix": "APP_"}
@lru_cache
def get_settings() -> Settings:
return Settings()
6. Strategy Pattern (Functional)
Replace string-flag branching with callable strategies.
from dataclasses import dataclass, field
from typing import Callable
@dataclass
class SupportTicket:
id: str = field(init=False, default_factory=generate_id)
customer: str
issue: str
type Ordering = Callable[[list[SupportTicket]], list[SupportTicket]]
def fifo_ordering(tickets: list[SupportTicket]) -> list[SupportTicket]:
return tickets.copy()
def filo_ordering(tickets: list[SupportTicket]) -> list[SupportTicket]:
return list(reversed(tickets))
def random_ordering(tickets: list[SupportTicket]) -> list[SupportTicket]:
return random.sample(tickets, len(tickets))
class CustomerSupport:
def __init__(self) -> None:
self.tickets: list[SupportTicket] = []
def create_ticket(self, customer: str, issue: str) -> None:
self.tickets.append(SupportTicket(customer=customer, issue=issue))
def process_tickets(self, ordering: Ordering) -> None:
for ticket in ordering(self.tickets):
self._process_ticket(ticket)
7. Observer Pattern (Events)
Decouple side effects from core business logic via events.
from collections import defaultdict
from typing import Callable, Any
subscribers: dict[str, list[Callable]] = defaultdict(list)
def subscribe(event_type: str, handler: Callable) -> None:
subscribers[event_type].append(handler)
def post_event(event_type: str, data: Any) -> None:
for handler in subscribers[event_type]:
handler(data)
# Business logic stays clean:
def register_new_user(name: str, password: str, email: str):
user = create_user(name, password, email)
post_event("user_registered", user)
# Listeners wired up separately:
def setup_email_handlers():
subscribe("user_registered", lambda u: send_welcome_email(u))
def setup_slack_handlers():
subscribe("user_registered", lambda u: notify_sales(u))
8. Template Method Pattern
Define algorithm skeleton in base class, let subclasses override specific steps.
from abc import ABC, abstractmethod
class TradingBot(ABC):
def check_prices(self, coin: str) -> None:
self.connect()
prices = self.get_market_data(coin)
if self.should_buy(prices):
print(f"Buy {coin}!")
elif self.should_sell(prices):
print(f"Sell {coin}!")
def connect(self) -> None:
print("Connecting to exchange...")
def get_market_data(self, coin: str) -> list[float]:
return [10, 12, 18, 14]
@abstractmethod
def should_buy(self, prices: list[float]) -> bool: ...
@abstractmethod
def should_sell(self, prices: list[float]) -> bool: ...
class AverageTrader(TradingBot):
def should_buy(self, prices: list[float]) -> bool:
return prices[-1] < sum(prices) / len(prices)
def should_sell(self, prices: list[float]) -> bool:
return prices[-1] > sum(prices) / len(prices)
9. Error Handling — Custom Exceptions
Use domain-specific exceptions, never generic Exception.
class NotFoundError(Exception):
pass
class NotAuthorizedError(Exception):
pass
def fetch_blog(blog_id: str) -> dict:
blog = db_fetch(blog_id)
if blog is None:
raise NotFoundError(f"Blog {blog_id} not found")
if not blog["public"]:
raise NotAuthorizedError(f"Blog {blog_id} is private")
return blog
# Flask: Map domain exceptions to HTTP status codes
@app.route("/blogs/<id>")
def get_blog(id: str):
try:
return jsonify(fetch_blog(id))
except NotFoundError:
abort(404, description="Resource not found")
except NotAuthorizedError:
abort(403, description="Access denied")
10. Coupling & Cohesion
Separate data, registration logic, and application orchestration.
# ✅ GOOD: Each class has a single, clear responsibility
@dataclass
class VehicleInfo:
brand: str
electric: bool
catalogue_price: int
def compute_tax(self) -> float:
rate = 0.02 if self.electric else 0.05
return rate * self.catalogue_price
@dataclass
class Vehicle:
id: str
license_plate: str
info: VehicleInfo
class VehicleRegistry:
def __init__(self) -> None:
self.vehicle_info: dict[str, VehicleInfo] = {}
def add_vehicle_info(self, brand: str, info: VehicleInfo) -> None:
self.vehicle_info[brand] = info
def generate_vehicle_id(self, length: int) -> str:
return "".join(random.choices(string.ascii_uppercase, k=length))
class Application:
def __init__(self, registry: VehicleRegistry) -> None:
self.registry = registry
def register_vehicle(self, brand: str) -> Vehicle:
info = self.registry.vehicle_info[brand]
vehicle_id = self.registry.generate_vehicle_id(12)
license_plate = vehicle_id[:2] + "-" + "".join(random.choices(string.digits, k=4))
return Vehicle(id=vehicle_id, license_plate=license_plate, info=info)
Project Structure (Golden Repo)
src/ ├── __init__.py ├── domain/ │ ├── __init__.py │ ├── models.py # Pydantic models, entities │ ├── events.py # Domain events │ ├── services.py # Domain services │ └── exceptions.py # Domain-specific exceptions ├── application/ │ ├── __init__.py │ ├── commands.py # Command handlers │ ├── queries.py # Query handlers │ └── ports.py # Abstract interfaces (Protocols) ├── infrastructure/ │ ├── __init__.py │ ├── database.py # Database setup & session │ ├── repositories.py # Repository implementations │ └── event_bus.py # Event bus implementation ├── api/ │ ├── __init__.py │ ├── main.py # FastAPI app │ ├── routes/ # API route modules │ ├── schemas.py # API request/response models │ └── dependencies.py # FastAPI dependency injection └── config.py # Settings
Best Practices
✅ Use from __future__ import annotations for PEP 604 syntax
✅ Prefer composition over inheritance
✅ Use Protocol for dependency interfaces (structural subtyping)
✅ Keep functions pure where possible
✅ Use @dataclass(frozen=True) for immutable value objects
✅ Apply the "Tell, Don't Ask" principle
✅ Use match statements for complex branching (Python 3.10+)
Anti-Patterns
❌ God objects that do everything
❌ Circular imports between modules
❌ Business logic in API routes
❌ Hard-coded configuration values
❌ Using dict instead of typed models
❌ Catching exceptions too broadly
❌ String flags for strategy selection (if mode == "fifo") — use callable/Protocol
❌ Side effects coupled to business logic (email in registration) — use events
❌ Hardcoded dependencies — inject via constructor
❌ Generic Exception in API layer — use domain-specific exception hierarchy
Example Prompts
- •"Refactor this code to use the repository pattern with dependency injection"
- •"Create a domain model with Pydantic for a booking system"
- •"Implement the Result pattern for error handling in this service"