Introduction to Python Web Frameworks
Python's web ecosystem is rich and diverse, offering frameworks that range from batteries-included monoliths to minimalist microframeworks. Among the most discussed options today are Django, FastAPI, and Sanic. Each was built to solve different problems, and choosing the right one can dramatically affect your project's performance, developer experience, and long-term maintainability.
This tutorial compares these three frameworks across architecture, performance, developer ergonomics, and real-world use cases. By the end, you'll understand when to reach for each one and how to build a basic endpoint in all three.
What Each Framework Is
Django: The Batteries-Included Giant
Django was first released in 2005 and has since become one of the most popular Python web frameworks. It follows the "batteries-included" philosophy, shipping with an ORM, authentication system, admin panel, form handling, templating engine, and migrations out of the box. Django is synchronous by default, though Django 3.0+ introduced limited async support.
FastAPI: The Modern Async API Builder
FastAPI, released in 2018 by Sebastián Ramírez, is built on top of Starlette and Pydantic. It leverages Python type hints to provide automatic request validation, serialization, and interactive API documentation via Swagger UI and ReDoc. FastAPI is async-first and designed specifically for building APIs.
Sanic: The Async Web Server
Sanic, released in 2016, was one of the first Python frameworks to embrace Python's asyncio for high-performance HTTP handling. Inspired by Flask's simplicity, Sanic runs on an async event loop and is designed to handle many concurrent connections efficiently. It positions itself as a fast, lightweight web server and framework.
Why Framework Choice Matters
The framework you choose affects several critical dimensions of your application:
- Performance: Async frameworks like Sanic and FastAPI can handle thousands of concurrent connections, while traditional Django uses a synchronous worker model that requires multiple processes or threads for concurrency.
- Developer Productivity: Django's admin panel and ORM accelerate CRUD-heavy applications. FastAPI's auto-generated docs and type validation speed up API development. Sanic offers Flask-like simplicity for those already familiar with that style.
- Ecosystem and Hiring: Django has the largest ecosystem and community. FastAPI is growing rapidly. Sanic has a smaller but dedicated community.
- Learning Curve: Django has many concepts to learn. FastAPI requires understanding type hints and async. Sanic is approachable if you know Flask and asyncio basics.
- Long-Term Maintenance: Mature frameworks like Django have stable APIs and extensive documentation, reducing maintenance risk.
Architecture Comparison
Django's MVT Architecture
Django uses the Model-View-Template (MVT) pattern. Models define database schema via the ORM, views contain business logic, and templates render HTML. Django also includes middleware, signals, and a URL dispatcher. The framework is opinionated about project structure.
FastAPI's Dependency Injection Model
FastAPI uses path operation decorators and a powerful dependency injection system. Request and response schemas are defined using Pydantic models. Dependencies can be nested and reused, making testing and code organization clean. The framework is unopinionated about database choice or project structure.
Sanic's Handler-Based Approach
Sanic uses a Flask-like routing system where you register handler functions or class-based views. It includes its own HTTP server built on uvloop, avoiding the need for a separate ASGI server in many cases. Sanic supports blueprints for organizing larger applications.
How to Use Each Framework
Setting Up a Django Project
Install Django and create a new project:
pip install django
django-admin startproject myproject
cd myproject
python manage.py startapp myapp
Define a simple view in myapp/views.py:
from django.http import JsonResponse
from django.views.decorators.http import require_GET
@require_GET
def hello(request):
name = request.GET.get("name", "World")
return JsonResponse({"message": f"Hello, {name}!"})
Wire up the URL in myproject/urls.py:
from django.urls import path
from myapp.views import hello
urlpatterns = [
path("hello/", hello),
]
Run the server:
python manage.py runserver
Setting Up a FastAPI Project
Install FastAPI and an ASGI server:
pip install fastapi uvicorn
Create main.py:
from fastapi import FastAPI, Query
app = FastAPI(title="My API")
@app.get("/hello")
async def hello(name: str = Query(default="World")):
return {"message": f"Hello, {name}!"}
Run the server:
uvicorn main:app --reload
FastAPI automatically generates interactive documentation at /docs and /redoc.
Setting Up a Sanic Project
Install Sanic:
pip install sanic
Create main.py:
from sanic import Sanic
from sanic.response import json
app = Sanic("MyApp")
@app.get("/hello")
async def hello(request):
name = request.args.get("name", "World")
return json({"message": f"Hello, {name}!"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
Run the server:
python main.py
Building a CRUD Endpoint: A Deeper Comparison
To illustrate the differences more concretely, let's build a simple in-memory item store with create and read operations in each framework.
FastAPI CRUD Example
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List
app = FastAPI()
class Item(BaseModel):
id: int
name: str
price: float
items: List[Item] = []
@app.get("/items", response_model=List[Item])
async def get_items():
return items
@app.post("/items", response_model=Item, status_code=201)
async def create_item(item: Item):
if any(i.id == item.id for i in items):
raise HTTPException(status_code=400, detail="Item already exists")
items.append(item)
return item
Notice how Pydantic handles validation automatically. If a client sends invalid JSON or missing fields, FastAPI returns a 422 error with detailed feedback.
Sanic CRUD Example
from sanic import Sanic, json
from sanic.exceptions import SanicException
app = Sanic("ItemsApp")
items = []
@app.get("/items")
async def get_items(request):
return json({"items": items})
@app.post("/items")
async def create_item(request):
data = request.json
if not data or "id" not in data or "name" not in data:
raise SanicException("Invalid payload", status_code=400)
if any(i["id"] == data["id"] for i in items):
raise SanicException("Item already exists", status_code=400)
items.append(data)
return json(data, status=201)
Sanic requires manual validation. You can integrate libraries like marshmallow or pydantic to add schema validation, but it is not built in.
Django CRUD Example
# myapp/models.py
from django.db import models
class Item(models.Model):
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
# myapp/views.py
from django.http import JsonResponse, HttpRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_GET, require_POST
import json
from .models import Item
@require_GET
def list_items(request: HttpRequest):
items = list(Item.objects.values("id", "name", "price"))
return JsonResponse({"items": items})
@require_POST
@csrf_exempt
def create_item(request: HttpRequest):
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
item = Item.objects.create(
name=data.get("name", ""),
price=data.get("price", 0),
)
return JsonResponse({"id": item.id, "name": item.name, "price": str(item.price)}, status=201)
# myproject/urls.py
from django.urls import path
from myapp.views import list_items, create_item
urlpatterns = [
path("items/", list_items),
path("items/create/", create_item),
]
Django's strength is the ORM. With a few lines, you get database persistence, migrations, and a model layer. However, building JSON APIs requires more boilerplate than FastAPI. For serious API work in Django, most developers use Django REST Framework.
Performance Considerations
Benchmark results vary by workload, but general patterns hold:
- FastAPI and Sanic both handle async I/O efficiently and can serve tens of thousands of requests per second on modest hardware for simple endpoints.
- Sanic often edges out FastAPI on raw throughput for simple JSON responses because it runs its own optimized HTTP server.
- FastAPI adds overhead from Pydantic validation but compensates with developer productivity and correctness guarantees.
- Django is significantly slower per request in default configurations but scales horizontally well. Using
gunicornwith multiple workers and an async-capable server likedaphneoruvicorncan improve throughput.
For I/O-bound workloads such as proxying to slow external APIs or streaming data, async frameworks shine. For CPU-bound workloads, the GIL limits all three, and you'll need multiprocessing regardless of framework.
Best Practices
General Best Practices
- Use environment variables for configuration via libraries like
python-dotenvorpydantic-settings. - Structure projects with clear separation between routes, business logic, and data access layers.
- Write tests early. All three frameworks provide test clients.
- Use a reverse proxy like Nginx or a managed service in production.
- Enable gzip compression and proper logging in production.
Django Best Practices
- Use Django REST Framework or Django Ninja for API development instead of writing raw views.
- Leverage Django's built-in admin for internal tools and data management.
- Use
select_relatedandprefetch_relatedto avoid N+1 queries. - Keep business logic in models or services, not in views.
- Use Django's migration system faithfully; never edit the database schema manually.
FastAPI Best Practices
- Define Pydantic models for all request and response payloads to leverage automatic validation.
- Use dependency injection for database sessions, authentication, and shared logic.
- Organize routes into APIRouters for modular codebases.
- Run behind Uvicorn or Hypercorn with multiple workers in production.
- Use background tasks or Celery for long-running operations to avoid blocking the event loop.
Sanic Best Practices
- Use Blueprints to organize routes into reusable modules.
- Avoid blocking calls inside handlers; use
asyncio-compatible libraries for database and HTTP access. - Use Sanic Extensions for features like OpenAPI documentation and dependency injection.
- Configure workers based on CPU cores using
app.run(workers=4)in production. - Implement custom error handlers for consistent API error responses.
When to Choose Each Framework
Choose Django When
- You are building a content-heavy site with admin interfaces, authentication, and forms.
- You need a mature ORM with migrations and a large ecosystem of reusable packages.
- Your team values convention over configuration and long-term stability.
- You are building a monolithic application with server-rendered templates or a traditional REST API.
Choose FastAPI When
- You are building a modern REST or GraphQL API with strict schema requirements.
- Automatic documentation and type safety are important to your team.
- You need high performance with async I/O and clean developer ergonomics.
- You are building microservices or machine learning model-serving endpoints.
Choose Sanic When
- You need maximum throughput for simple HTTP handling.
- You prefer a Flask-like API but require async support.
- You are building real-time applications, websockets, or streaming services.
- You want a self-contained server without depending on an external ASGI server.
Conclusion
Django, FastAPI, and Sanic each occupy a distinct niche in the Python web ecosystem. Django remains the best choice for full-featured applications that benefit from a mature ORM, admin panel, and decades of community support. FastAPI has rapidly become the go-to framework for modern, type-safe APIs with automatic documentation and excellent async performance. Sanic offers a compelling option for developers who want Flask-like simplicity combined with high-throughput async handling. The right choice depends on your project's requirements: pick Django for batteries-included completeness, FastAPI for API-first development with strong typing, and Sanic for lightweight, high-performance async services. Whichever you choose, following the best practices outlined above will help you build maintainable, scalable applications that stand the test of time.