はじめに
Claude Codeに「FastAPIのユーザー取得エンドポイントを書いて」と頼むと、動いているように見えるが何かがおかしいコードが返ってくることがある。session.query(User).filter(User.id == id).first()という同期スタイルのSQLAlchemy、user.dict()というPydantic v1の書き方。どちらも2024年以前の正解で、今は非推奨か動かない。
これはClaude Codeの能力の問題ではなく、学習データのバイアスの問題だ。インターネット上のPythonコードはPydantic v1やSQLAlchemy 1.x時代のものが圧倒的に多く、明示的に指定しなければ古いパターンが出てくる。解決策は単純で、「何を使うか」をCLAUDE.mdに書けば良い。
Claude CodeがPythonの旧コードを出す理由
問題が起きるのは決まったパターンだ。
# Claude Codeがデフォルトで生成しがちなコード
class User(BaseModel):
name: str
class Config:
orm_mode = True # Pydantic v1のパターン
user.dict() # v1の.dict()、v2では非推奨
# 正しいPydantic v2パターン
class User(BaseModel):
name: str
model_config = ConfigDict(from_attributes=True)
user.model_dump()
SQLAlchemyでも同じ問題が起きる。Session()とsession.query()の組み合わせは1.x時代の書き方で、2.x asyncでは使えない。CLAUDE.mdにバージョンと禁止パターンを書くと、これが解消される。
CLAUDE.mdでバージョン問題を根絶する
# プロジェクト技術スタック
Python環境
- Python 3.12 / FastAPI 0.110+
- SQLAlchemy 2.x(asyncのみ。syncパターンは絶対に使わない)
- Pydantic v2(model_dump()を使う。.dict()は禁止)
- pytest 8.x + pytest-asyncio
アーキテクチャルール
- router → service → repository の3層構造
- Pydantic モデル: ConfigDict(from_attributes=True) を使用
- SQLAlchemy: expire_on_commit=False を AsyncSession に設定
禁止パターン
- from sqlalchemy.orm import Session(sync)→ AsyncSession を使う
- user.dict() → user.model_dump() を使う
- orm_mode = True → ConfigDict(from_attributes=True) を使う
「禁止パターン」セクションの効果が特に大きい。何をしてはいけないかを書くと、Claude Codeが旧パターンを選びにくくなる。Zennのdiscus0434氏が公開しているPythonテンプレート(discus0434/python-template-for-claude-code)では、ここにさらに型ヒント必須・docstring不要を追加している。
SQLAlchemy 2.x asyncで地雷になりやすいのが、AsyncSession環境でのLazy Loadingだ。user.ordersのように関連モデルを遅延取得しようとするとMissingGreenletErrorが発生する。CLAUDE.mdにselectinloadを使うことと明記するだけで、このパターンが正しく出るようになる。
# selectinloadで事前フェッチ
result = await session.execute(
select(User).options(selectinload(User.orders)).where(User.id == user_id)
)
user = result.scalar_one()
PostToolUseフックでRuffを自動化する
フックの仕組み全般については「Claude Code Hooks・Skills・Schedulerで定常業務を自動操縦にする」を参照。ここではPython/FastAPI固有のRuff自動化設定に絞る。
Claude Codeがファイルを書き込むたびに自動でRuffが走る設定は、.claude/settings.jsonに数行書くだけで実現できる(PostToolUseフックはClaude Code公式ドキュメントで確認済み)。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "python .claude/hooks/py_format.py" }]
}
]
}
}
呼び出されるスクリプト(py_format.py)は.pyファイルへの書き込み時にRuffのフォーマットとlint自動修正を実行する。
import json, subprocess, sys
data = json.load(sys.stdin)
file_path = data.get("tool_input", {}).get("file_path", "")
if file_path.endswith(".py"):
subprocess.run(["ruff", "format", file_path], check=False)
subprocess.run(["ruff", "check", "--fix", file_path], check=False)
チームでリポジトリを共有していれば、全員に同じフック設定が適用される。
pytest-asyncio × httpx でテストを量産する
テスト生成でもCLAUDE.mdの設定が効く。pytest-asyncio + httpx.AsyncClient でE2Eテストを書くと明記しておくと、テストの雛形をほぼ手直しなしで使えるレベルで出してくれるようになる。
フィクスチャはインメモリSQLite(sqlite+aiosqlite:///:memory:)を使い、テスト終了後にスキーマごと破棄する構成にする。clientフィクスチャでFastAPIの依存関係をテスト用DBに差し替えると、実際のDBを汚さずにE2Eテストが書ける。
@pytest.fixture
async def client(db_session):
app.dependency_overrides[get_db] = lambda: db_session
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
yield ac
app.dependency_overrides.clear()
テスト自体はAAAパターン(Arrange-Act-Assert)で書かせると整理しやすい。
@pytest.mark.asyncio
async def test_create_user(client):
# Arrange
payload = {"name": "Alice", "email": "alice@example.com"}
# Act
response = await client.post("/users", json=payload)
# Assert
assert response.status_code == 201
assert response.json()["name"] == "Alice"
実践報告では、5関数程度のモジュールに対して15〜25テストを2分以内に生成できたという事例がある(Suganthi、Medium 2025年。単一ソースの参考値であり、実際の生成数はコードの複雑さによって変わる)。
まとめ
最初からこれを整備しているプロジェクトと、何も設定していないプロジェクトでは、Claude Codeが出すコードの質がはっきり変わる。CLAUDE.mdへの追記は10分、フックの設定は5分程度でできる。

コメント