Claude Code for Python Development: From Scripts to Production
Using Claude Code for Python development — FastAPI, Django, SQLAlchemy, pytest, type hints, async patterns, and production-grade Python with AI assistance.
Python and Claude Code: A Strong Combination
Python is Claude Code's strongest language. This is not coincidental — the SWE-bench benchmark that Claude Code scored 80.9% on is entirely Python-based. Claude Code's training included extensive Python codebases, and its tool system (Bash, Read, Edit) integrates naturally with Python's ecosystem of CLI tools, testing frameworks, and package managers.
CLAUDE.md for Python Projects
# Python Project Configuration
## Environment
- Python 3.12
- Package manager: uv (preferred) or pip
- Virtual environment: .venv/ (always activate before running)
- Linting: ruff (replaces flake8, isort, black)
- Type checking: mypy --strict
## Framework: FastAPI
- All endpoints in app/api/v1/
- Business logic in app/services/
- Database models in app/models/
- Pydantic schemas in app/schemas/
- Dependencies in app/deps.py
## Conventions
- Use async/await everywhere — no sync code in request handlers
- Type hints on all function signatures (parameters and return types)
- Use Annotated[type, Depends(dep)] for dependency injection
- Pydantic v2 with model_config = ConfigDict(from_attributes=True)
- Never use import * — always explicit imports
- Use pathlib.Path instead of os.path
## Testing
- Framework: pytest with pytest-asyncio
- Run tests: pytest -x --tb=short -q
- Fixtures in conftest.py at each test directory level
- Use factory functions for test data, not fixtures for every model
- Mock external services only — never mock the database
## Database
- ORM: SQLAlchemy 2.0 with async engine
- Migrations: Alembic
- Always use async sessions: AsyncSession
- Use select() syntax, not legacy Query API
FastAPI Patterns
Claude Code generates clean FastAPI code when it understands your patterns.
Dependency Injection with Annotated Types
# app/deps.py
from typing import Annotated, AsyncGenerator
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import async_session_factory
from app.models.user import User
from app.services.auth import AuthService
security = HTTPBearer()
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session_factory() as session:
yield session
async def get_current_user(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> User:
auth_service = AuthService(db)
user = await auth_service.verify_token(credentials.credentials)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
)
return user
# Type aliases for clean endpoint signatures
DB = Annotated[AsyncSession, Depends(get_db)]
CurrentUser = Annotated[User, Depends(get_current_user)]
# app/api/v1/projects.py
from fastapi import APIRouter, HTTPException, status
from app.deps import DB, CurrentUser
from app.schemas.project import CreateProjectRequest, ProjectResponse, ProjectListResponse
from app.services.project import ProjectService
router = APIRouter(prefix="/projects", tags=["projects"])
@router.get("", response_model=ProjectListResponse)
async def list_projects(
db: DB,
user: CurrentUser,
page: int = 1,
limit: int = 20,
):
service = ProjectService(db)
return await service.list_for_user(user.id, page=page, limit=limit)
@router.post("", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
async def create_project(
request: CreateProjectRequest,
db: DB,
user: CurrentUser,
):
service = ProjectService(db)
return await service.create(user_id=user.id, data=request)
SQLAlchemy 2.0 Async Patterns
Claude Code generates modern SQLAlchemy 2.0 syntax when your CLAUDE.md specifies it:
# app/services/project.py
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.project import Project
from app.schemas.project import CreateProjectRequest
class ProjectService:
def __init__(self, db: AsyncSession):
self.db = db
async def list_for_user(
self, user_id: str, page: int = 1, limit: int = 20
) -> dict:
offset = (page - 1) * limit
# Count query
count_stmt = (
select(func.count())
.select_from(Project)
.where(Project.owner_id == user_id)
)
total = await self.db.scalar(count_stmt) or 0
# Data query with eager loading
data_stmt = (
select(Project)
.where(Project.owner_id == user_id)
.options(selectinload(Project.team))
.order_by(Project.created_at.desc())
.offset(offset)
.limit(limit)
)
result = await self.db.execute(data_stmt)
projects = list(result.scalars().all())
return {
"data": projects,
"pagination": {
"page": page,
"limit": limit,
"total": total,
"total_pages": (total + limit - 1) // limit,
},
}
async def create(self, user_id: str, data: CreateProjectRequest) -> Project:
project = Project(
owner_id=user_id,
**data.model_dump(),
)
self.db.add(project)
await self.db.commit()
await self.db.refresh(project)
return project
Pytest Patterns
Claude Code writes comprehensive tests when prompted:
# tests/conftest.py
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from app.main import app
from app.deps import get_db
from app.models.base import Base
TEST_DATABASE_URL = "postgresql+asyncpg://test:test@localhost/test_db"
@pytest_asyncio.fixture
async def db_session():
engine = create_async_engine(TEST_DATABASE_URL)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
session_factory = async_sessionmaker(engine, expire_on_commit=False)
async with session_factory() as session:
yield session
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await engine.dispose()
@pytest_asyncio.fixture
async def client(db_session: AsyncSession):
async def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
yield client
app.dependency_overrides.clear()
# tests/api/test_projects.py
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_create_project(client: AsyncClient, auth_headers: dict):
response = await client.post(
"/api/v1/projects",
json={
"name": "Test Project",
"description": "A test project",
"visibility": "private",
},
headers=auth_headers,
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Test Project"
assert data["visibility"] == "private"
assert "id" in data
@pytest.mark.asyncio
async def test_create_project_validation(client: AsyncClient, auth_headers: dict):
response = await client.post(
"/api/v1/projects",
json={"description": "Missing required name field"},
headers=auth_headers,
)
assert response.status_code == 422
@pytest.mark.asyncio
async def test_list_projects_pagination(client: AsyncClient, auth_headers: dict):
# Create 25 projects
for i in range(25):
await client.post(
"/api/v1/projects",
json={"name": f"Project {i}", "visibility": "private"},
headers=auth_headers,
)
# First page
response = await client.get(
"/api/v1/projects?page=1&limit=10",
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert len(data["data"]) == 10
assert data["pagination"]["total"] == 25
assert data["pagination"]["total_pages"] == 3
Django Patterns
Claude Code also generates quality Django code:
# Django REST Framework viewset
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Q
from .models import Project
from .serializers import ProjectSerializer, ProjectCreateSerializer
class ProjectViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Project.objects.filter(
Q(owner=self.request.user) | Q(team__members=self.request.user)
).select_related("owner", "team").distinct()
def get_serializer_class(self):
if self.action == "create":
return ProjectCreateSerializer
return ProjectSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
@action(detail=True, methods=["post"])
def archive(self, request, pk=None):
project = self.get_object()
if project.owner != request.user:
return Response(
{"error": "Only the owner can archive a project"},
status=status.HTTP_403_FORBIDDEN,
)
project.is_archived = True
project.save(update_fields=["is_archived", "updated_at"])
return Response(ProjectSerializer(project).data)
Python-Specific Prompts That Work Well
| Task | Prompt |
|---|---|
| Add type hints | "Add complete type annotations to all functions in app/services/user.py" |
| Async conversion | "Convert this sync SQLAlchemy code to async using AsyncSession" |
| Test generation | "Write pytest tests for UserService covering all public methods and edge cases" |
| Pydantic schema | "Create Pydantic v2 schemas for the User model with create, update, and response variants" |
| Migration | "Create an Alembic migration to add a status column to the projects table" |
| Error handling | "Add proper error handling to all endpoints in app/api/v1/users.py using HTTPException" |
Debugging Python with Claude Code
Claude Code excels at Python debugging:
The following test is failing:
pytest tests/api/test_projects.py::test_create_project -x -v
Error:
sqlalchemy.exc.IntegrityError: (asyncpg.UniqueViolationError) duplicate key
value violates unique constraint "projects_name_team_id_key"
Find the root cause and fix it.
Claude Code will trace the issue through the test fixtures, find that test data is not being cleaned up properly between tests, and fix the fixture isolation.
Conclusion
Python development with Claude Code benefits from Claude's deep training on Python codebases. The key to getting production-quality output is a thorough CLAUDE.md that specifies your framework patterns (SQLAlchemy 2.0 async, Pydantic v2, modern pytest), your conventions (type hints everywhere, no sync code), and your project structure. With these in place, Claude Code generates Python that passes mypy strict mode, follows your patterns, and includes proper error handling and test coverage.
NYC News
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.