Web Development

Medium50 min read

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
FrameworkStyleBest for
FastAPIAsync, type-driven, OpenAPIModern APIs, microservices
FlaskMinimal, sync, plugin ecosystemSmall APIs and apps
DjangoFull-stack, batteries includedServer-rendered sites, admin
StarletteLow-level ASGI toolkitCustom 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 ...".