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.
| Feature | WSGI (Web Server Gateway Interface) | ASGI (Asynchronous Server Gateway Interface) |
|---|---|---|
| Model | Synchronous | Asynchronous (async/await) |
| Server | Gunicorn, uWSGI | Uvicorn, Hypercorn, Daphne |
| Framework | Flask, Django | FastAPI, Django (3.0+), Starlette |
| Best for | Regular request-response | WebSocket, long-polling, streaming |
| Concurrency | Multi-process / Multi-thread | Event 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)"]
endhttp.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 useapp.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 — A Full-Featured Framework #
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 #
| Feature | http.server | Flask | FastAPI | Django |
|---|---|---|---|---|
| Purpose | Static files | REST API | REST API | Full-stack |
| Interface | WSGI | WSGI | ASGI | WSGI/ASGI |
| Learning curve | Very easy | Easy | Easy-medium | Medium-high |
| Input validation | ✗ | Manual | Automatic | Automatic (DRF) |
| API documentation | ✗ | Manual | Automatic | Manual/DRF |
| Built-in ORM | ✗ | ✗ | ✗ | ✓ |
| Admin panel | ✗ | ✗ | ✗ | ✓ |
| Production | ✗ | Gunicorn | Uvicorn | Gunicorn/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.serveris 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.