The Python Web Landscape
Why Web Development Matters
The Problem: Custom HTTP code is full of edge cases — CORS, content negotiation, validation, async, OpenAPI — and you'll re-invent them all if you start from scratch.
The Solution: FastAPI gives type-driven request validation, automatic OpenAPI docs, async by default, and Pydantic models for clean boundaries. Flask is the minimal alternative; Django is the batteries-included one.
Real Impact: Choosing the right framework and using its primitives correctly is 80% of being a productive Python web developer.
Real-World Analogy
Think of a web framework as a restaurant front-of-house:
- Route = the menu — what dishes are available at which URLs
- Pydantic model = the order ticket — validates what the customer asked for
- Dependency = the prep cook — gets ingredients ready before the chef starts
- Middleware = the host who greets and routes every customer
- Background task = the staff cleaning the table after the customer leaves
| Framework | Style | Best for |
|---|---|---|
| FastAPI | Async, type-driven, OpenAPI | Modern APIs, microservices |
| Flask | Minimal, sync, plugin ecosystem | Small APIs and apps |
| Django | Full-stack, batteries included | Server-rendered sites, admin |
| Starlette | Low-level ASGI toolkit | Custom frameworks |
FastAPI — Modern Async APIs
$ pip install "fastapi[standard]"
$ fastapi dev main.py # auto-reload dev server
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
app = FastAPI()
class User(BaseModel):
name: str
email: EmailStr
age: int = 0
@app.get("/")
async def root():
return {"hello": "world"}
@app.post("/users", response_model=User)
async def create_user(user: User):
if user.age < 0:
raise HTTPException(400, "age must be non-negative")
return user
@app.get("/users/{uid}")
async def get_user(uid: int):
return {"id": uid}
FastAPI auto-generates OpenAPI/Swagger docs at /docs and ReDoc at /redoc based on your type hints and Pydantic models. No extra config needed.
Pydantic — Data Validation
Pydantic powers FastAPI's request validation. Use it standalone too — anywhere you parse external data into typed objects.
from pydantic import BaseModel, Field, EmailStr, field_validator
class SignupRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: EmailStr
age: int = Field(ge=13, le=130)
tags: list[str] = Field(default_factory=list)
@field_validator("name")
@classmethod
def no_spaces(cls, v: str):
if " " in v:
raise ValueError("name must not contain spaces")
return v
# Validates and coerces in one step
req = SignupRequest.model_validate({
"name": "alice", "email": "[email protected]", "age": "30" # str age coerced to int
})
Flask — Minimal and Pluggable
$ pip install flask
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/")
def root():
return jsonify({"hello": "world"})
@app.route("/users", methods=["POST"])
def create_user():
data = request.get_json()
if not data.get("email"):
return jsonify({"error": "email required"}), 400
return jsonify(data), 201
if __name__ == "__main__":
app.run(debug=True)
Flask is sync by default. For async I/O, prefer FastAPI; or use Flask with asgiref shims.
Django — Batteries Included
$ pip install django
$ django-admin startproject mysite
$ cd mysite
$ python manage.py startapp blog
$ python manage.py runserver
Models, Views, Templates
# blog/models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
# blog/views.py
from django.shortcuts import render
def post_list(request):
posts = Post.objects.order_by("-created_at")[:10]
return render(request, "blog/list.html", {"posts": posts})
Django ships an ORM, admin interface, auth, forms, templates, migrations, and CSRF protection. Great for server-rendered sites; can also serve APIs via Django REST Framework.
Middleware, Dependency Injection, Background Tasks
FastAPI Dependencies
from fastapi import Depends
async def get_db():
async with AsyncSession(engine) as s:
yield s
@app.get("/users/{uid}")
async def get_user(uid: int, db = Depends(get_db)):
return await db.scalar(select(User).where(User.id == uid))
Background Tasks
from fastapi import BackgroundTasks
@app.post("/signup")
async def signup(email: str, bg: BackgroundTasks):
user = await create_user(email)
bg.add_task(send_welcome_email, email) # runs after response sent
return user
For heavy background work, use a real task queue: Celery, RQ, Dramatiq, or arq (async).
🎯 Practice Exercises
Exercise 1: REST API in FastAPI
Build CRUD endpoints for a tasks resource with Pydantic models. Verify the OpenAPI docs render at /docs.
Exercise 2: Pydantic validation
Define a BookingRequest with date ranges, emails, and constraints. Test edge cases (past dates, invalid email).
Exercise 3: Flask blog
One-file Flask app with in-memory posts. Add list, detail, create endpoints.
Exercise 4: Background email
POST to /register triggers a background task that prints "sending welcome email to ...".