Claude Code's output quality for Python projects depends heavily on how well you configure it. With a solid CLAUDE.md, type-aware hooks, and clear patterns for async SQLAlchemy and Pydantic v2, it becomes an exceptional FastAPI development tool.
1. CLAUDE.md for FastAPI Projects
# Project: my-fastapi
## Stack
- Python 3.12 + FastAPI 0.110
- SQLAlchemy 2.x (async) + Alembic
- Pydantic v2
- PostgreSQL 16
## Commands
- Run: `uvicorn app.main:app --reload`
- Test: `pytest -v`
- Lint: `ruff check . && mypy .`
- Migration: `alembic upgrade head`
## Architecture
- Layers: Router → Service → Repository
- No DB calls in Routers
- No HTTP types in Services
- DI via FastAPI's Depends()
## Code Rules
- Type annotations required (mypy strict)
- No `Any` type without justification comment
- No `print()` in production code (use loguru)
- All async functions must handle errors
## Security
- All SQL via SQLAlchemy ORM (no raw strings)
- All external input validated with Pydantic
- Secrets only via python-dotenv, never hardcoded
- No passwords/tokens in logs
## Testing
- pytest + pytest-asyncio
- `@pytest.mark.asyncio` decorator required
- DB: SQLite in-memory for tests
- Pattern: AAA (Arrange, Act, Assert)
2. SQLAlchemy 2.x Async Patterns
Without explicit instructions, Claude Code sometimes generates old-style sync SQLAlchemy code. Add this to CLAUDE.md:
## SQLAlchemy Rules
- Use AsyncSession (never sync Session)
- Use `select()` style queries (not old query() style)
- Use `selectinload()` for eager loading
- Always use AsyncSession as context manager
With this, "fetch all posts for a user" generates correct async code:
async def get_user_posts(db: AsyncSession, user_id: int) -> list[Post]:
result = await db.execute(
select(Post)
.where(Post.user_id == user_id)
.options(selectinload(Post.author))
.order_by(Post.created_at.desc())
)
return list(result.scalars().all())
3. Pydantic v2 Patterns
Pydantic v1 and v2 have very different APIs. Make your version explicit:
## Pydantic Rules
- Use Pydantic v2 (no v1 @validator decorators)
- Use `model_config = ConfigDict(from_attributes=True)` for ORM models
- Use `model_validator` and `field_validator` (v2 style)
- Use `model_dump()` not `.dict()`
Generated schema example:
from pydantic import BaseModel, ConfigDict, field_validator
class UserCreate(BaseModel):
email: str
name: str
@field_validator('email')
@classmethod
def validate_email(cls, v: str) -> str:
if '@' not in v:
raise ValueError('Invalid email format')
return v.lower()
class UserResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
email: str
name: str
4. Auto-Format Hook with Ruff
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "python .claude/hooks/py_format.py"
}]
}
]
}
}
# .claude/hooks/py_format.py
import json, subprocess, sys
data = json.load(sys.stdin)
fp = data.get("tool_input", {}).get("file_path", "")
if fp and fp.endswith(".py"):
subprocess.run(["ruff", "format", fp], capture_output=True)
subprocess.run(["ruff", "check", "--fix", fp], capture_output=True)
sys.exit(0)
Every .py file gets auto-formatted on write.
5. Test Generation
Specify your test setup clearly:
## Testing Details
- Framework: pytest + pytest-asyncio
- HTTP client: httpx.AsyncClient
- DB: SQLite in-memory
- Fixtures in conftest.py: async_session, async_client
Then /test-gen app/services/user.py generates:
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_create_user(async_client: AsyncClient):
payload = {"email": "test@example.com", "name": "Test User"}
response = await async_client.post("/api/v1/users", json=payload)
assert response.status_code == 201
assert response.json()["email"] == payload["email"]
assert "id" in response.json()
@pytest.mark.asyncio
async def test_create_user_invalid_email(async_client: AsyncClient):
payload = {"email": "not-an-email", "name": "Test"}
response = await async_client.post("/api/v1/users", json=payload)
assert response.status_code == 422
6. Python Anti-Patterns Claude Code Catches
With a well-written CLAUDE.md and /code-review:
| Issue | Example |
|---|---|
| Mutable default args | def fn(items=[]) |
| Bare except | except: pass |
| SELECT * queries | Performance + N+1 |
| print() in production | Log pollution |
| Missing await | Coroutine never executed |
Setup Checklist
□ CLAUDE.md with stack, commands, architecture rules
□ SQLAlchemy 2.x async patterns documented
□ Pydantic v2 specified
□ pytest-asyncio patterns documented
□ ruff configured (pyproject.toml)
□ Auto-format hook for .py files
□ /code-review skill for Python
Pre-built skills for Python projects: Code Review Pack (¥980) includes /code-review, /refactor-suggest, and /test-gen with Python/FastAPI support.
Myouga (@myougatheaxo) — Security-focused Claude Code engineer.
Top comments (0)