Skip to content
Back to Blog
Agentic AI6 min read

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.

Share this article
N

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.