Claude Code for Python Web Development: FastAPI, Flask, and Django — Claude Skills 360 Blog
Blog / Development / Claude Code for Python Web Development: FastAPI, Flask, and Django
Development

Claude Code for Python Web Development: FastAPI, Flask, and Django

Published: May 7, 2026
Read time: 9 min read
By: Claude Skills 360

Python web development spans three dominant frameworks with different philosophies: FastAPI for modern async APIs, Flask for lightweight apps and microservices, Django for full-featured applications. Claude Code works well across all three because it reads your project structure and generates framework-idiomatic code rather than generic Python.

This guide covers using Claude Code for Python web development — FastAPI patterns, Pydantic models, async database access, Flask routing, Django ORM patterns, and testing.

Setting Up Claude Code for Python Web Projects

Framework context in CLAUDE.md prevents a lot of wrong-guess code generation:

# Python Web Project Context

## Stack
- Python 3.12, FastAPI 0.110, uvicorn
- Database: PostgreSQL via SQLAlchemy 2.0 async + alembic
- Validation: Pydantic v2 (model_config, not class Config)
- Auth: JWT via python-jose + passlib for password hashing
- Testing: pytest + httpx.AsyncClient for async endpoint testing

## Conventions
- All endpoints async (async def)
- Pydantic models: separate schema per operation (CreateUser, UserResponse, UpdateUser)
- Database sessions via dependency injection (Depends(get_db))
- Errors: raise HTTPException with detail dict, not string
- No global state — inject everything via FastAPI Depends

## Never
- Synchronous database calls in async endpoints
- Mutable default arguments
- Generic exception catches (except Exception) — be specific

See the CLAUDE.md setup guide for full configuration patterns.

FastAPI: REST API Development

Defining Models and Endpoints

Create a CRUD API for a blog post resource.
Fields: title, content, published (bool, default false), author_id.
Use separate Pydantic models for create, update, and response.
from pydantic import BaseModel, ConfigDict
from datetime import datetime
from typing import Optional

class PostCreate(BaseModel):
    title: str
    content: str
    published: bool = False

class PostUpdate(BaseModel):
    title: Optional[str] = None
    content: Optional[str] = None
    published: Optional[bool] = None

class PostResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    id: int
    title: str
    content: str
    published: bool
    author_id: int
    created_at: datetime
    updated_at: datetime

@router.post("/posts", response_model=PostResponse, status_code=201)
async def create_post(
    post: PostCreate,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
) -> PostResponse:
    db_post = Post(**post.model_dump(), author_id=current_user.id)
    db.add(db_post)
    await db.commit()
    await db.refresh(db_post)
    return db_post

Claude generates three separate Pydantic models (create, update, response) following FastAPI’s recommended pattern. ConfigDict(from_attributes=True) is the Pydantic v2 way — not the v1 class Config: orm_mode = True.

Dependency Injection

Write a FastAPI dependency that extracts the current user from a JWT token.
Return 401 if missing/invalid, 403 if user is inactive.
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

security = HTTPBearer()

async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security),
    db: AsyncSession = Depends(get_db),
) -> User:
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail={"code": "invalid_credentials", "message": "Could not validate credentials"},
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
        user_id: int = payload.get("sub")
        if user_id is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    
    user = await db.get(User, user_id)
    if user is None:
        raise credentials_exception
    if not user.is_active:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail={"code": "inactive_user", "message": "User account is deactivated"},
        )
    return user

Async Database Patterns

Write an async repository function that fetches a user's posts with 
pagination and filtering by published status.
Use SQLAlchemy 2.0 async style.
async def get_posts(
    db: AsyncSession,
    author_id: int,
    published_only: bool = False,
    page: int = 1,
    page_size: int = 20,
) -> tuple[list[Post], int]:
    query = select(Post).where(Post.author_id == author_id)
    
    if published_only:
        query = query.where(Post.published == True)
    
    # Total count
    count_result = await db.execute(select(func.count()).select_from(query.subquery()))
    total = count_result.scalar_one()
    
    # Paginated results
    query = query.order_by(Post.created_at.desc())
    query = query.offset((page - 1) * page_size).limit(page_size)
    result = await db.execute(query)
    
    return result.scalars().all(), total

SQLAlchemy 2.0 uses select() (not the legacy query() API), and async sessions require await db.execute(). Claude generates 2.0-style code rather than the legacy ORM patterns that most tutorials show.

Background Tasks

After a user registers, send a welcome email without blocking the response.
Use FastAPI's BackgroundTasks.
@router.post("/auth/register", response_model=UserResponse, status_code=201)
async def register(
    user_data: UserCreate,
    background_tasks: BackgroundTasks,
    db: AsyncSession = Depends(get_db),
) -> UserResponse:
    user = await create_user(db, user_data)
    background_tasks.add_task(send_welcome_email, user.email, user.name)
    return user

FastAPI’s BackgroundTasks runs after the response is sent — no blocking. For heavy async work, Claude escalates to a proper task queue (Celery, ARQ) and explains the tradeoff.

Error Handling

Add a global exception handler that returns consistent error JSON 
for all HTTPExceptions and unhandled exceptions.
Never expose stack traces to clients.
from fastapi import Request
from fastapi.responses import JSONResponse

@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
    # exc.detail might be a dict (our format) or string (FastAPI defaults)
    if isinstance(exc.detail, dict):
        detail = exc.detail
    else:
        detail = {"message": exc.detail}
    
    return JSONResponse(
        status_code=exc.status_code,
        content={"error": detail},
    )

@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
    logger.exception("Unhandled exception", extra={"path": request.url.path})
    return JSONResponse(
        status_code=500,
        content={"error": {"message": "Internal server error"}},
    )

The logger call with exc_info=True (from logger.exception) captures the full stack trace in your logs without exposing it to clients.

Flask: Lightweight APIs and Microservices

Flask is the right choice for simple internal APIs, microservices, and cases where Django’s weight isn’t justified.

Blueprint Structure

Structure a Flask API with blueprints.
Services: auth (login, register, refresh), users (CRUD), 
and payments (webhook handler).

Claude generates the blueprint pattern where each service is its own module:

# app/__init__.py
from flask import Flask
from .auth.routes import auth_bp
from .users.routes import users_bp
from .payments.routes import payments_bp

def create_app(config_name: str = "production") -> Flask:
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    
    app.register_blueprint(auth_bp, url_prefix="/api/auth")
    app.register_blueprint(users_bp, url_prefix="/api/users")
    app.register_blueprint(payments_bp, url_prefix="/api/payments")
    
    return app

The factory pattern (create_app) is important for testing — you can create fresh app instances with TESTING=True config. Claude generates this by default for Flask projects.

Flask Request Validation

Add input validation to Flask routes. 
We want Pydantic-style validation without switching to FastAPI.

Claude integrates Flask with Pydantic:

from functools import wraps
from flask import request, jsonify
from pydantic import BaseModel, ValidationError

def validate_body(schema_class):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            try:
                data = schema_class.model_validate(request.get_json() or {})
                return f(*args, data=data, **kwargs)
            except ValidationError as e:
                return jsonify({"error": {"validation": e.errors()}}), 422
        return wrapper
    return decorator

@users_bp.post("/")
@validate_body(UserCreate)
def create_user(data: UserCreate):
    user = UserService.create(data)
    return jsonify(user.model_dump()), 201

Django ORM Queries

Write a Django ORM query for the admin dashboard:
users grouped by signup month, with total count and 
count of users who made a purchase within 30 days of signup.
from django.db.models import Count, Q, F
from django.db.models.functions import TruncMonth
from datetime import timedelta

def get_signup_cohort_data():
    return (
        User.objects.annotate(
            signup_month=TruncMonth('date_joined')
        )
        .values('signup_month')
        .annotate(
            total_users=Count('id'),
            converted_users=Count(
                'id',
                filter=Q(
                    orders__created_at__lte=F('date_joined') + timedelta(days=30)
                )
            )
        )
        .order_by('signup_month')
    )

Django ORM’s annotate() + filter=Q(...) pattern handles conditional aggregations. Claude generates correct Django ORM — not raw SQL — and knows when to use select_related vs prefetch_related for avoiding N+1.

Django REST Framework

Create a DRF ViewSet for the Order model.
Include: list (own orders only), retrieve, create, and cancel (custom action).

Claude generates a ModelViewSet with get_queryset scoped to the authenticated user, a @action decorator for the cancel endpoint, and a PermissionDenied if the user tries to cancel someone else’s order.

Testing Python Web APIs

The testing guide covers general patterns. For FastAPI specifically:

Write pytest tests for the POST /posts endpoint.
Cover: success (201), validation error (422), unauthenticated (401).
Use httpx.AsyncClient.
import pytest
from httpx import AsyncClient

@pytest.mark.asyncio
async def test_create_post_success(client: AsyncClient, auth_headers: dict):
    response = await client.post(
        "/posts",
        json={"title": "Test Post", "content": "Content here", "published": False},
        headers=auth_headers,
    )
    assert response.status_code == 201
    data = response.json()
    assert data["title"] == "Test Post"
    assert "id" in data

@pytest.mark.asyncio
async def test_create_post_validation_error(client: AsyncClient, auth_headers: dict):
    response = await client.post(
        "/posts",
        json={"content": "Missing title"},
        headers=auth_headers,
    )
    assert response.status_code == 422

@pytest.mark.asyncio
async def test_create_post_unauthenticated(client: AsyncClient):
    response = await client.post("/posts", json={"title": "Test", "content": "Test"})
    assert response.status_code == 401

Claude correctly uses httpx.AsyncClient (not the sync TestClient) for async FastAPI apps, generates a conftest.py with proper async fixtures, and scopes the test database to a transaction that rolls back after each test.

Python Web Development with Claude Code

The combination of FastAPI’s type hints + Pydantic v2 + Python type annotations gives Claude Code much better signal about your intent. When you write:

async def get_user(user_id: int, db: AsyncSession = Depends(get_db)) -> UserResponse:

Claude can infer the expected input, database pattern, and return type without you explaining any of it. This makes Python web development especially productive — the type annotations are both documentation and specification.

For database patterns (migrations with Alembic, N+1 queries in SQLAlchemy, query optimization), see the database guide. For the full builder workflow with Server Actions and SSR on Python backends, the Claude Skills 360 bundle includes FastAPI, Flask, and Django skill sets covering common patterns: authentication, file uploads, webhooks, and admin panels. Start with the free tier to try the API scaffolding patterns.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free