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: Full-Featured Applications
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.