Web Servers #

Python has a rich web server ecosystem — from built-in modules for simple needs to full-fledged frameworks for production-scale applications. Choosing the right one depends on project complexity, performance needs, and how many “batteries” you want included. This article covers four main options with realistic-enough examples to show each one’s character: http.server, Flask, FastAPI, and Django.

WSGI vs ASGI #

Before choosing a framework, it’s important to understand the two Python web interface models, because they affect performance and the deployment ecosystem.

FeatureWSGI (Web Server Gateway Interface)ASGI (Asynchronous Server Gateway Interface)
ModelSynchronousAsynchronous (async/await)
ServerGunicorn, uWSGIUvicorn, Hypercorn, Daphne
FrameworkFlask, DjangoFastAPI, Django (3.0+), Starlette
Best forRegular request-responseWebSocket, long-polling, streaming
ConcurrencyMulti-process / Multi-threadEvent loop (more efficient I/O)

To visualize how request handling differs in concurrency between WSGI and ASGI architectures, compare the flow diagrams below:

flowchart TD
    subgraph WSGIModel ["WSGI Concurrency Model"]
        Client1["Client 1"] --> ServerWSGI["WSGI Server (Gunicorn)"]
        Client2["Client 2"] --> ServerWSGI
        ServerWSGI -->|"Thread 1 / Worker 1"| App1["Flask / Django App"]
        ServerWSGI -->|"Thread 2 / Worker 2"| App2["Flask / Django App"]
    end
    subgraph ASGIModel ["ASGI Concurrency Model"]
        ClientA["Client A (WebSocket / HTTP)"] --> ServerASGI["ASGI Server (Uvicorn)"]
        ClientB["Client B (WebSocket / HTTP)"] --> ServerASGI
        ServerASGI -->|"Event Loop (Single Thread)"| AsyncApp["FastAPI App (Async/Await)"]
    end

http.server — Quick Development Server #

Python’s built-in module, no installation needed. Useful for serving static files temporarily during development — not for production.

import http.server
import socketserver

PORT = 8000

# SimpleHTTPRequestHandler: serve files from the current directory
with socketserver.TCPServer(("", PORT), http.server.SimpleHTTPRequestHandler) as httpd:
    print(f"Serving at http://localhost:{PORT}")
    httpd.serve_forever()

Or directly from the terminal without writing any code:

# Python 3 — serve the current directory on port 8000
python -m http.server 8000

# Specify a specific directory
python -m http.server 8000 --directory /path/to/folder
http.server is not suitable for production — no authentication, no security, and very limited performance. Use it only for local development or temporarily sharing files on an internal network.

Flask — A Flexible Microframework #

Flask gives you a minimal foundation: routing and request/response handling. You choose everything else yourself — database, authentication, validation. Good for simple to medium REST APIs, or when you want full control over the architecture.

pip install flask

Routing and Responses #

from flask import Flask, request, jsonify, abort

app = Flask(__name__)

# Simulated data (normally from a database)
product_db = {
    1: {"id": 1, "name": "Laptop", "price": 12000000},
    2: {"id": 2, "name": "Mouse", "price": 250000},
}

@app.route("/products", methods=["GET"])
def list_products():
    """GET /products — return all products."""
    return jsonify(list(product_db.values()))

@app.route("/products/<int:product_id>", methods=["GET"])
def product_detail(product_id):
    """GET /products/:id — return one product by ID."""
    product = product_db.get(product_id)
    if product is None:
        abort(404)  # Flask will return a 404 response automatically
    return jsonify(product)

@app.route("/products", methods=["POST"])
def add_product():
    """POST /products — add a new product from the JSON body."""
    data = request.get_json()
    if not data or "name" not in data or "price" not in data:
        return jsonify({"error": "Fields 'name' and 'price' are required."}), 400

    new_id = max(product_db.keys()) + 1
    new_product = {"id": new_id, "name": data["name"], "price": data["price"]}
    product_db[new_id] = new_product
    return jsonify(new_product), 201  # 201 Created

@app.route("/products/<int:product_id>", methods=["DELETE"])
def delete_product(product_id):
    """DELETE /products/:id — delete a product."""
    if product_id not in product_db:
        abort(404)
    del product_db[product_id]
    return "", 204  # 204 No Content

if __name__ == "__main__":
    # debug=True is only for development — turn it off in production!
    app.run(host="0.0.0.0", port=5000, debug=True)

Centralized Error Handling #

@app.errorhandler(404)
def not_found(error):
    return jsonify({"error": "Resource not found."}), 404

@app.errorhandler(500)
def server_error(error):
    return jsonify({"error": "An internal server error occurred."}), 500
Don’t use app.run() in production. Flask’s development server is single-threaded and not secure. For production, run Flask with a WSGI server like Gunicorn: gunicorn -w 4 -b 0.0.0.0:5000 app:app (4 worker processes).

FastAPI — Modern APIs with Type Hints #

FastAPI leverages Python type hints for automatic input validation, response serialization, and auto-generated interactive documentation. Its performance matches Node.js and Go for I/O-bound tasks thanks to ASGI.

pip install fastapi uvicorn

Routing, Validation, and Automatic Docs #

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
from typing import Optional

app = FastAPI(title="Product API", version="1.0.0")

# Pydantic model: definition plus validation and documentation
class ProductCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100, description="Product name")
    price: int = Field(..., gt=0, description="Price in rupiah, must be positive")
    stock: Optional[int] = Field(default=0, ge=0)

class Product(ProductCreate):
    id: int

# Simulated data
product_db: dict[int, Product] = {
    1: Product(id=1, name="Laptop", price=12000000, stock=5),
    2: Product(id=2, name="Mouse", price=250000, stock=20),
}

@app.get("/products", response_model=list[Product])
def list_products(skip: int = 0, limit: int = 10):
    """
    Fetch a list of products with pagination.
    - **skip**: number of items to skip
    - **limit**: maximum number of items returned
    """
    items = list(product_db.values())
    return items[skip : skip + limit]

@app.get("/products/{product_id}", response_model=Product)
def product_detail(product_id: int):
    """Fetch one product's details by ID."""
    if product_id not in product_db:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Product with ID {product_id} not found."
        )
    return product_db[product_id]

@app.post("/products", response_model=Product, status_code=status.HTTP_201_CREATED)
def add_product(product: ProductCreate):
    """
    Add a new product.
    FastAPI automatically validates the request body against ProductCreate.
    If validation fails, 422 Unprocessable Entity is returned automatically.
    """
    new_id = max(product_db.keys()) + 1
    new_product = Product(id=new_id, **product.model_dump())
    product_db[new_id] = new_product
    return new_product

@app.delete("/products/{product_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_product(product_id: int):
    """Delete a product by ID."""
    if product_id not in product_db:
        raise HTTPException(status_code=404, detail="Product not found.")
    del product_db[product_id]

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

Once the server is running, FastAPI provides interactive documentation automatically:

  • Swagger UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc
FastAPI uses Pydantic for validation — if the request body doesn’t match the schema, FastAPI automatically returns 422 Unprocessable Entity with details about which field is wrong, without you writing any manual validation code.

Django comes with “batteries included”: ORM, admin panel, authentication, form handling, migrations — everything is already there. Good for complex web applications needing many ready-made features.

pip install django djangorestframework

Creating a Project and App #

django-admin startproject store .    # create the project (dot = in the current directory)
python manage.py startapp product    # create an app
python manage.py migrate            # run the initial migrations
python manage.py runserver          # run the development server

The generated project structure:

store/
  ├── manage.py
  ├── store/
  │   ├── settings.py      ← project configuration
  │   ├── urls.py          ← main routing
  │   └── wsgi.py
  └── product/
      ├── models.py        ← model/database table definitions
      ├── views.py         ← request/response logic
      ├── urls.py          ← app routing
      └── admin.py         ← admin panel configuration

Simple Model and View Example #

# product/models.py
from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.PositiveIntegerField()
    stock = models.PositiveIntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

    class Meta:
        ordering = ["-created_at"]
# product/views.py — using Django REST Framework
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import serializers
from .models import Product

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = "__all__"

class ProductViewSet(viewsets.ModelViewSet):
    """
    A ViewSet automatically provides:
    GET    /products/       → list all products
    POST   /products/       → add a new product
    GET    /products/{id}/  → one product's details
    PUT    /products/{id}/  → update a product
    DELETE /products/{id}/  → delete a product
    """
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
# Create and run migrations after defining the model
python manage.py makemigrations product
python manage.py migrate

Comparison and Selection Guide #

Featurehttp.serverFlaskFastAPIDjango
PurposeStatic filesREST APIREST APIFull-stack
InterfaceWSGIWSGIASGIWSGI/ASGI
Learning curveVery easyEasyEasy-mediumMedium-high
Input validationManualAutomaticAutomatic (DRF)
API documentationManualAutomaticManual/DRF
Built-in ORM
Admin panel
ProductionGunicornUvicornGunicorn/Uvicorn
Choose Flask when:
  ✓ You need a simple REST API with full control
  ✓ The team is already familiar with Flask
  ✓ You don't need automatic validation or documentation
  ✓ You want the flexibility to choose your own components

Choose FastAPI when:
  ✓ You need a REST API with strict validation and automatic documentation
  ✓ High performance for I/O-bound tasks
  ✓ The team is comfortable with type hints and async/await
  ✓ You want API docs (Swagger) without extra effort

Choose Django when:
  ✓ You need a complete web app: auth, admin, ORM, forms
  ✓ You want "batteries included" without assembling things yourself
  ✓ A large-scale project with a big team
  ✓ You need a customizable admin panel

Summary #

  • http.server is only for local development — not secure or performant enough for production.
  • Flask is a flexible WSGI microframework — you choose all the components yourself; good for simple to medium REST APIs.
  • FastAPI is a modern ASGI framework — automatic input validation via Pydantic, auto-generated Swagger docs, and high performance; great for APIs needing type safety and the best developer experience.
  • Django ships with everything ready to use — ORM, admin, authentication, migrations; great for complex web apps needing high productivity from day one.
  • Don’t use Flask’s app.run() in production — use Gunicorn for WSGI, Uvicorn for ASGI.
  • WSGI (Flask, Django) for synchronous request-response; ASGI (FastAPI, Django 3+) for WebSocket, streaming, and async handlers.
  • Choose a framework based on project needs, not popularity — each has different trade-offs.

← Previous: WebSockets   Next: Unit Testing →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact