DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Python/FastAPI Development with Claude Code: CLAUDE.md Setup, Hooks, and Best Practices

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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()`
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

4. Auto-Format Hook with Ruff

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{
          "type": "command",
          "command": "python .claude/hooks/py_format.py"
        }]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode
# .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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Pre-built skills for Python projects: Code Review Pack (¥980) includes /code-review, /refactor-suggest, and /test-gen with Python/FastAPI support.

👉 prompt-works.jp

Myouga (@myougatheaxo) — Security-focused Claude Code engineer.

Top comments (0)