You've set up Claude Code or Cursor. You ask it to write a FastAPI endpoint. It comes back with dict everywhere, bare except Exception, and a function that mutates a list default argument. It works, but it's not Python — it's Python-shaped code that will hurt you in six months.
The fix is a CLAUDE.md that tells the model exactly what "good Python" means on your project. Here are 13 rules that turn AI-generated Python from plausible to production-ready.
1. Always use type hints — including Annotated and TypeVar
# Bad
def get_user(user_id):
...
# Good
from typing import Annotated
from uuid import UUID
UserId = Annotated[UUID, "user primary key"]
def get_user(user_id: UserId) -> User:
...
Rule for CLAUDE.md: "All functions must have fully annotated signatures. Use Annotated for domain-specific constraints. Define TypeVar for generic utilities."
2. Use Pydantic models, not raw dicts
Dicts are untyped bags. Pydantic models are contracts.
# Bad
def create_order(data: dict) -> dict:
...
# Good
from pydantic import BaseModel, Field
class OrderCreate(BaseModel):
product_id: UUID
quantity: int = Field(gt=0)
class OrderResponse(BaseModel):
id: UUID
total_cents: int
Rule: "Never use dict for structured data at function boundaries. Define Pydantic models for all request/response shapes."
3. Use pathlib, not os.path
# Bad
import os
config_path = os.path.join(os.path.dirname(__file__), "config.json")
# Good
from pathlib import Path
config_path = Path(__file__).parent / "config.json"
Rule: "pathlib.Path for all filesystem operations. Never import os.path."
4. f-strings for all string formatting
# Bad
msg = "User %s has %d items" % (name, count)
msg = "User {} has {} items".format(name, count)
# Good
msg = f"User {name} has {count} items"
Rule: "f-strings exclusively. No % formatting, no .format() calls."
5. dataclasses or attrs for data containers
When you don't need Pydantic validation, use dataclasses for lightweight containers:
from dataclasses import dataclass, field
from typing import List
@dataclass
class Pipeline:
name: str
steps: List[str] = field(default_factory=list)
enabled: bool = True
Rule: "Use @dataclass for internal data containers that don't need Pydantic validation. Always use field(default_factory=...) for mutable defaults."
6. Never use mutable default arguments
This is Python's most famous footgun. AI models reproduce it constantly.
# Bad — shared across all calls
def add_item(item: str, items: list = []) -> list:
items.append(item)
return items
# Good
def add_item(item: str, items: list | None = None) -> list:
if items is None:
items = []
items.append(item)
return items
Rule: "No mutable default arguments ([], {}, custom objects). Use None and initialize inside the function body."
7. Proper async/await — no sync blocking in async functions
# Bad — blocks the event loop
async def fetch_data(url: str) -> bytes:
import urllib.request
return urllib.request.urlopen(url).read()
# Good
import httpx
async def fetch_data(url: str) -> bytes:
async with httpx.AsyncClient() as client:
response = await client.get(url)
response.raise_for_status()
return response.content
Rule: "Async functions must never call blocking I/O. Use httpx.AsyncClient for HTTP, asyncio.to_thread for CPU-bound work."
8. Exception chaining with raise X from Y
# Bad — loses original traceback
try:
result = db.query(sql)
except Exception:
raise RuntimeError("Database query failed")
# Good
try:
result = db.query(sql)
except psycopg2.Error as e:
raise DatabaseError(f"Query failed: {sql[:100]}") from e
Rule: "Always chain exceptions with raise NewError(...) from original_error. Never use bare except Exception: raise RuntimeError(...)."
9. logging, never print
# Bad
print(f"Processing {item_id}...")
print(f"ERROR: {e}")
# Good
import logging
logger = logging.getLogger(__name__)
logger.info("Processing item", extra={"item_id": item_id})
logger.exception("Processing failed", extra={"item_id": item_id})
Rule: "No print() statements in production code. Use module-level logger = logging.getLogger(__name__). Use logger.exception() inside except blocks to capture tracebacks."
10. Virtual environments and explicit dependency pinning
Rule: "Always assume a virtual environment. Dependencies go in pyproject.toml (preferred) or requirements.txt with pinned versions. Never suggest pip install X without adding to dependency file."
# pyproject.toml
[project]
dependencies = [
"fastapi>=0.111.0,<0.112",
"pydantic>=2.7.0,<3",
"httpx>=0.27.0,<0.28",
]
11. pytest fixtures over setup/teardown
# Bad
class TestUserService(unittest.TestCase):
def setUp(self):
self.db = create_test_db()
# Good
import pytest
@pytest.fixture
def db():
database = create_test_db()
yield database
database.rollback()
def test_create_user(db: Database) -> None:
user = create_user(db, email="test@example.com")
assert user.id is not None
Rule: "pytest only — no unittest.TestCase. Use fixtures for setup/teardown. Test functions are plain functions, never methods."
12. Google-style docstrings, one-liners only for simple functions
def calculate_discount(price: float, rate: float) -> float:
"""Apply discount rate to price."""
return price * (1 - rate)
def process_order(order: OrderCreate, db: Database) -> OrderResponse:
"""Create an order and return the persisted result.
Args:
order: Validated order input from the request body.
db: Active database session, injected by FastAPI dependency.
Returns:
Persisted order with generated ID and computed totals.
Raises:
ProductNotFoundError: If `order.product_id` does not exist.
"""
...
Rule: "Google-style docstrings for public functions. One-liner for simple helpers. Never generate NumPy-style docstrings."
13. ruff and mypy are the law
Rule: "All generated code must pass ruff check (E, F, I rules) and mypy --strict. Never disable type checks with # type: ignore without a comment explaining why."
# These must pass before any code is considered done
ruff check src/
mypy src/ --strict
Suggested pyproject.toml section:
[tool.mypy]
strict = true
ignore_missing_imports = false
[tool.ruff]
select = ["E", "F", "I", "UP", "B"]
Your CLAUDE.md block
Drop this into your project's CLAUDE.md:
## Python Standards
- Type hints required on all functions; use `Annotated` for domain types
- Pydantic models for all structured data at boundaries — no raw dicts
- `pathlib.Path` for filesystem; never `os.path`
- f-strings only; no `%` or `.format()`
- `@dataclass` with `field(default_factory=...)` for mutable defaults
- No mutable default arguments — use `None` sentinel
- Async functions: no blocking I/O; use `httpx.AsyncClient`, `asyncio.to_thread`
- Exception chaining: `raise NewError(...) from original`
- `logging.getLogger(__name__)` only; no print statements
- Dependencies in `pyproject.toml` with pinned ranges
- pytest fixtures; no `unittest.TestCase`
- Google-style docstrings on public functions
- Code must pass `ruff check` and `mypy --strict`
These 13 rules push AI from "writes Python" to "writes your Python." The goal isn't to restrict the model — it's to give it enough context that it makes the right call the first time, every time.
These rules are pre-applied in the CLAUDE.md Rules Pack — 50+ production rules for Python, TypeScript, Go, Rust, Swift, and more. $27 one-time.
Top comments (0)