Python Backend Cheatsheet: Django, Django REST Framework, FastAPI, and Flask
Python powers some of the busiest web backends in the world. The ecosystem offers four dominant choices — Django, Django REST Framework, FastAPI, and Flask — each with a distinct philosophy. This article covers architecture, core patterns, async/sync tradeoffs, authentication, and deployment, then distils the essentials into a practical cheatsheet and a side-by-side comparison to help you pick the right tool.
For the surrounding infrastructure — reverse proxies, process managers, and containerisation — see the DevOps Ready Reckoner.
Choosing a Python Web Framework
| Framework | Batteries-included? | Built-in admin | Async | API-first | Typical use |
|---|---|---|---|---|---|
| Django | Yes | Yes | Partial | No (adds DRF) | Monoliths, CMS, admin panels |
| Django REST Framework | On top of Django | Yes | Partial | Yes | REST APIs on Django |
| FastAPI | No | No | Yes | Yes | Modern APIs, microservices, ML services |
| Flask | No | No | Limited | No (extensions) | Small APIs, prototypes, glue |
Architecture: Sync, Async, and ASGI/WSGI
All four frameworks can be served over WSGI. FastAPI and modern Django also support ASGI, which enables async handlers and WebSocket endpoints.
WSGI -> requests processed synchronously (one thread per request)
ASGI -> requests can run asynchronously (event loop)
also supports WebSockets and background tasks
The request flow is conceptually:
Client
|
HTTP request
|
Server (gunicorn / uvicorn / daphne)
|
Router -> View/Handler -> Business logic -> Model -> Database
|
Response body
|
Client
Django
app per bounded context, each with its own models.py, views.py, urls.py, and tests.py. The manage.py CLI drives almost everything.Project layout
myblog/
manage.py
myblog/
__init__.py
settings.py
urls.py
wsgi.py
asgi.py
blog/
__init__.py
admin.py
apps.py
models.py
tests.py
views.py
Settings excerpt
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
...
'blog',
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydb',
'USER': 'myuser',
'PASSWORD': '***',
'HOST': 'db',
'PORT': '5432',
}
}
Models and migrations
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
published_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
python manage.py makemigrations
python manage.py migrate
Views, templates, and URLs
from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404
from .models import Post
def post_list(request):
posts = Post.objects.all()
return render(request, 'blog/post_list.html', {'posts': posts})
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
return render(request, 'blog/post_detail.html', {'post': post})
# blog/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.post_list, name='post_list'),
path('post//', views.post_detail, name='post_detail'),
]
# blog/templates/blog/post_list.html
{% extends 'base.html' %}
{% block content %}
{% for post in posts %}
{{ post.title }}
{{ post.body }}
{% endfor %}
{% endblock %}
Django REST Framework
DRF layers a RESTful API and automatic OpenAPI schema generation on top of a Django project.
# serializers.py
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = '__all__'
# views.py
from rest_framework import viewsets
from rest_framework.response import Response
from .models import Post
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
ModelViewSet gives you list, create, retrieve, update, and destroy for free. Override individual actions when business rules diverge.FastAPI
FastAPI uses Python type hints to define routes, validate requests, and generate documentation automatically.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List
app = FastAPI(title="My API")
class PostIn(BaseModel):
title: str
body: str
class Post(PostIn):
id: int
DB: List[Post] = []
@app.get("/posts", response_model=List[Post])
def list_posts():
return DB
@app.get("/posts/{post_id}", response_model=Post)
def read_post(post_id: int):
for post in DB:
if post.id == post_id:
return post
raise HTTPException(status_code=404, detail="Not found")
@app.post("/posts", response_model=Post, status_code=201)
def create_post(payload: PostIn):
post = Post(id=len(DB) + 1, **payload.dict())
DB.append(post)
return post
Flask
Flask is a micro-framework: a routing layer, Jinja2 templating, and a development server. Everything else is an extension.
from flask import Flask, jsonify, request, abort
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
db = SQLAlchemy(app)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200))
body = db.Column(db.Text)
@app.route("/posts")
def list_posts():
posts = Post.query.all()
return jsonify([{"id": p.id, "title": p.title, "body": p.body} for p in posts])
if __name__ == "__main__":
app.run(debug=True)
Flask keeps you close to the metal. That gives you freedom — and responsibility.
Authentication and Authorization
Authentication answers "who is this?", authorization answers "what may they do?".
| Framework | Auth mechanism | Notes |
|---|---|---|
| Django | django.contrib.auth | Users, groups, permissions built in |
| DRF | DRF tokens, JWT, Session | IsAuthenticated, DjangoModelPermissions |
| FastAPI | fastapi-users, JWT, OAuth2 | Depends-based security utilities |
| Flask | Flask-Login, Flask-JWT | Extensions |
Common Patterns
Dependency injection (FastAPI)
from fastapi import Depends
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/posts")
def list_posts(db: Session = Depends(get_db)):
return db.query(Post).all()
Middleware and request hooks
# FastAPI middleware
@app.middleware("http")
async def add_process_header(request: Request, call_next):
response = await call_next(request)
response.headers["X-Process-Time"] = "0.42s"
return response
Validation and Serialisation
All four frameworks can validate data. Pydantic (FastAPI, and usable standalone) and DRF serializers are the dominant approaches.
| Approach | Validation | Serialisation |
|---|---|---|
| Django forms | Yes | Form rendering |
| DRF serializers | Yes | To/from model |
| Pydantic (FastAPI) | Type-driven | Model -> dict |
| Flask-WTF | Yes | Form rendering |
Testing
# Django
from django.test import TestCase, Client
from .models import Post
class PostTests(TestCase):
def test_list(self):
client = Client()
response = client.get("/")
self.assertEqual(response.status_code, 200)
# FastAPI / Flask (pytest + httpx / flask testing)
from fastapi.testclient import TestClient
client = TestClient(app)
def test_list_posts():
response = client.get("/posts")
assert response.status_code == 200
Deployment
Run behind a production WSGI/ASGI server and a reverse proxy (nginx) for TLS termination and static assets.
Python app
|
WSGI/ASGI server (gunicorn, uvicorn, daphne)
|
Reverse proxy (nginx)
|
Load balancer / Internet
| Combination | Server | Notes |
|---|---|---|
| Django / Flask | gunicorn | WSGI workers |
| Django async | daphne + gunicorn | ASGI |
| FastAPI | uvicorn + gunicorn | workers=uvicorn.workers.UvicornWorker |
| Django (modern) | uvicorn + asgiref | ASGI path |
# gunicorn for Django/Flask
gunicorn myproject.wsgi:application \
--workers 4 --bind 0.0.0.0:8000
# uvicorn for FastAPI
gunicorn myapp.main:app \
-k uvicorn.workers.UvicornWorker \
--workers 4 --bind 0.0.0.0:8000
Python Backend Cheatsheet
Bootstrapping a project
# Django
django-admin startproject mysite
cd mysite
python manage.py startapp blog
python manage.py migrate
python manage.py runserver
# Flask
pip install flask
flask --app app run
# FastAPI
pip install "fastapi[all]"
uvicorn app:app --reload
Adding a record
# Django ORM
Post.objects.create(title="Hi", body="World")
# DRF: validated through serializer
PostSerializer(data={...}).save()
# SQLAlchemy (Flask/FastAPI pattern)
db.add(Post(title="Hi")); db.commit()
Running the development server
# Django
python manage.py runserver
# Flask
flask --app app run --debug
# FastAPI
uvicorn app:app --reload
Migrations and schema
# Django
python manage.py makemigrations
python manage.py migrate
# Flask-Migrate
flask db migrate
flask db upgrade
manage.py runserver, flask run, or uvicorn --reload in production. Use a production server (gunicorn/uvicorn/daphne) behind nginx.Framework Comparison
| Dimension | Django | DRF | FastAPI | Flask |
|---|---|---|---|---|
| Built-in admin | Yes | Yes | No | No |
| Built-in ORM | Yes | Yes | No | No |
| Built-in auth | Yes | Django auth | No | No |
| Async support | Partial | Partial | Yes | Limited |
| Auto docs (OpenAPI) | No | Yes | Yes | No |
| Type validation | Forms | Serializers | Pydantic | Flask-WTF |
| Routing style | Function/class | Function/class | Decorators | Decorators |
| CLI | manage.py | manage.py | Custom (typer/click) | flask CLI |
| Learning curve | Moderate-High | Moderate | Moderate | Low |
| Ecosystem size | Huge | Huge | Growing fast | Huge |
| Performance (typical) | Good | Good | Very good | Good |
When to Choose Which
| Scenario | Pick | Why |
|---|---|---|
| Admin panel / CMS | Django | Built-in admin and ORM |
| REST API on existing data model | DRF | Serializer + permission layer over Django |
| Microservices / async I/O | FastAPI | ASGI, type-driven validation, auto docs |
| Small glue service / prototype | Flask | Minimal, pick your own stack |
| Machine learning model serving | FastAPI | Async, Pydantic, pydantic-settings |
| Heavy legacy DB, no schema control | Django | Unmanaged models |
| Performance-critical API | FastAPI | uvicorn + type-driven validation |
Conclusion
Python's web ecosystem gives you a tool for almost every job: Django when you want batteries included, Django REST Framework when you are already on Django and need an API layer, FastAPI when performance, async, or type safety matter, and Flask when you want to choose every dependency yourself.
The most important choice is not which framework has the best benchmark — it is which framework your team can operate safely, patch, and reason about in production. Whichever you choose, standardise on one WSGI/ASGI server, one reverse proxy pattern, one secrets store, one test strategy, and one deployment pipeline. The framework is the small part; the operations around it are what keep systems secure and reliable.
This can also be extended into a Part 2: Python Deployment Cheatsheet, covering nginx gunicorn configs, systemd unit files, Let's Encrypt, health checks, and container images for Django, DRF, FastAPI, and Flask.