
Django vs Flask vs FastAPI: Which Python Web Framework Should You Choose?
Python has three web frameworks that between them cover most new projects: Django, Flask, and FastAPI. All three are mature, well documented, and used in production by large companies, so you won't go badly wrong with any of them. But they make very different assumptions about what you're building and how much the framework should decide for you, and picking the one that matches your project saves a lot of friction later.
This post compares them on the things that actually change your day-to-day work: how much comes in the box, how they handle request validation, async support, databases, and what a small endpoint looks like in each. Then it gets concrete about which one to pick for common kinds of projects.
The examples were run with Django 6.1, Flask 3.1, and FastAPI 0.142 on Python 3.13.
The Short Version
- Django is a full-stack framework with batteries included: ORM, migrations, admin, auth, forms, templates. Pick it for database-backed websites and products where you want those pieces to work together on day one.
- Flask is a micro-framework: routing, request/response, templates, and little else. Pick it when you want a small, explicit app and are happy to choose your own database and form libraries.
- FastAPI is an API framework built around type hints: you declare request and response shapes with Pydantic models, and it validates input and generates OpenAPI docs automatically. Pick it for JSON APIs, especially ones with lots of I/O or concurrency.
Philosophy: How Much Does the Framework Decide?
The biggest difference isn't speed or syntax. It's scope.
Django decides a lot. There's one ORM, one way to define URLs, one migration system, one admin, one settings module. A new developer joining a Django project knows where models, views, and templates live before they open a file. The cost is that you work Django's way; swapping out the ORM, for example, means giving up the admin, model forms, and much of the third-party ecosystem.
Flask decides very little. It gives you routing, a request object, sessions, and Jinja templates. Database access, form validation, auth, and project structure are up to you, usually via extensions such as Flask-SQLAlchemy, Flask-WTF, and Flask-Login. Two Flask codebases can look completely different.
FastAPI sits in between, but on a different axis. It's focused on APIs, and it decides how you describe data: Python type hints and Pydantic models. It doesn't include an ORM, admin, or template-driven forms, but it does include validation, serialization, dependency injection, and interactive API documentation, which are exactly the parts an API needs.
The Same Endpoint Three Ways
The clearest way to feel the difference is to build the same thing in each: a tiny books API with one endpoint to fetch a book and one to create a book from JSON, rejecting bad input. In-memory dictionaries stand in for a database.
Flask
# flask_app.py
from flask import Flask, abort, request
app = Flask(__name__)
BOOKS = {1: {"id": 1, "title": "Dune", "year": 1965}}
@app.get("/books/<int:book_id>")
def get_book(book_id: int):
book = BOOKS.get(book_id)
if book is None:
abort(404)
return book
@app.post("/books")
def create_book():
data = request.get_json(silent=True) or {}
title = data.get("title")
year = data.get("year")
if not isinstance(title, str) or not title.strip():
return {"error": "title is required"}, 422
if not isinstance(year, int):
return {"error": "year must be an integer"}, 422
book = {"id": len(BOOKS) + 1, "title": title.strip(), "year": year}
BOOKS[book["id"]] = book
return book, 201
Compact and readable. Returning a dict produces JSON. But all of the validation is hand-written, and the type hint on book_id is documentation only; it's the <int:...> converter that enforces it. A client sending "year": "1965" (a string) gets a 422, as intended, but only because you remembered to check.
Django
# books/views.py
import json
from django.http import Http404, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_GET, require_POST
BOOKS = {1: {"id": 1, "title": "Dune", "year": 1965}}
@require_GET
def get_book(request, book_id: int):
book = BOOKS.get(book_id)
if book is None:
raise Http404("Book not found")
return JsonResponse(book)
@csrf_exempt # a JSON API authenticated by token, not cookies
@require_POST
def create_book(request):
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "invalid JSON"}, status=400)
title, year = data.get("title"), data.get("year")
if not isinstance(title, str) or not title.strip():
return JsonResponse({"error": "title is required"}, status=422)
if not isinstance(year, int):
return JsonResponse({"error": "year must be an integer"}, status=422)
book = {"id": len(BOOKS) + 1, "title": title.strip(), "year": year}
BOOKS[book["id"]] = book
return JsonResponse(book, status=201)
# books/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("books/<int:book_id>", views.get_book),
path("books", views.create_book),
]
This is plain Django, and it's the least pleasant of the three for a JSON API. It also lives inside a project with settings, an app registry, and a root URLconf, which isn't shown. That's not a fair picture of Django, though: nobody builds serious Django APIs this way. You'd add Django REST Framework (serializers, viewsets, browsable API) or Django Ninja (a FastAPI-style, type-hint-driven layer on top of Django), and the code shrinks to something close to the FastAPI version while keeping the ORM and admin. Where Django really shines is everything around this endpoint: the Book model, its migrations, an admin screen to edit books, and user accounts, all in a few dozen lines.
FastAPI
# fastapi_app.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI()
class BookIn(BaseModel):
title: str = Field(min_length=1)
year: int
class Book(BookIn):
id: int
BOOKS: dict[int, Book] = {1: Book(id=1, title="Dune", year=1965)}
@app.get("/books/{book_id}")
def get_book(book_id: int) -> Book:
book = BOOKS.get(book_id)
if book is None:
raise HTTPException(status_code=404, detail="Book not found")
return book
@app.post("/books", status_code=201)
def create_book(payload: BookIn) -> Book:
book = Book(id=len(BOOKS) + 1, **payload.model_dump())
BOOKS[book.id] = book
return book
Here the type hints do real work. book_id: int is parsed and validated from the path. payload: BookIn tells FastAPI to read the JSON body and validate it against the model. The -> Book return annotation controls how the response is serialized and documented. There's no hand-written validation at all, and the errors are detailed:
{
"detail": [
{
"type": "int_parsing",
"loc": ["body", "year"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "soon"
}
]
}
One behavior to be aware of: Pydantic's default "lax" mode coerces compatible input, so "year": "1815" is accepted and converted to the integer 1815. Usually that's what you want from an API; if it isn't, Pydantic has a strict mode. (More on that in data validation with Pydantic.)
You also get interactive documentation at /docs and /redoc, generated from the same type hints, with no extra code.
Feature Comparison
| Django | Flask | FastAPI | |
|---|---|---|---|
| Type | Full-stack framework | Micro-framework | API framework |
| Server interface | WSGI and ASGI | WSGI | ASGI |
| ORM | Built in (Django ORM) | None (commonly SQLAlchemy) | None (commonly SQLAlchemy or SQLModel) |
| Migrations | Built in | Via Alembic / Flask-Migrate | Via Alembic |
| Admin interface | Built in | Extensions (e.g. Flask-Admin) | Third-party only |
| Auth / users | Built in | Extensions (e.g. Flask-Login) | Building blocks (OAuth2 helpers, dependencies) |
| HTML templates | Django templates (Jinja2 supported) | Jinja2 | Jinja2 via Starlette, rarely the focus |
| Request validation | Forms / DRF serializers | Manual or extensions | Pydantic, built in |
| API docs (OpenAPI) | Via DRF/Ninja add-ons | Via extensions | Automatic |
| Async views | Supported | Supported, but no concurrency gain under WSGI | Native |
| Learning curve | Steeper (lots of concepts) | Gentle | Gentle, if you know type hints |
Async: Where the Frameworks Really Differ
Async matters when a request spends most of its time waiting: calling other APIs, talking to slow services, streaming, WebSockets, long-polling. With an async framework, a single process can juggle many waiting requests at once instead of tying up a thread per request. (For background, see asyncio in Python.)
- FastAPI is async-native. It runs on an ASGI server such as Uvicorn, and
async defendpoints run on the event loop. You can also write plaindefendpoints; FastAPI runs those in a thread pool so blocking code doesn't stall the loop. WebSockets and streaming responses are first class. - Django supports
async defviews when served over ASGI, and its ORM has async methods such asaget(),acreate(), andasync forover querysets. It works, but much of the wider Django ecosystem is still sync-first, and mixing the two needs some care (sync_to_asyncandasync_to_syncexist for exactly that). For WebSockets, most projects use Django Channels. - Flask lets you write
async defviews if you installflask[async], but Flask is still a WSGI framework: each request occupies a worker for its full duration, and the async view just runs its own event loop inside that worker. It's handy for calling async libraries, not for serving more concurrent requests. If you want Flask's API on an async foundation, Quart is the Flask-compatible ASGI alternative.
If your workload is a classic CRUD website, request-per-thread is fine, and async isn't a deciding factor. If you're building something that holds thousands of connections open or fans out to many upstream services per request, FastAPI's model is a better fit.
Performance: Less Important Than It Looks
Benchmarks usually show FastAPI handling more trivial requests per second than Flask or Django. That's real, but it measures framework overhead on endpoints that do nothing. In a real app, time goes to database queries, network calls, template rendering, and serialization, and those dominate the framework's own cost by a wide margin.
What moves the needle in practice:
- Efficient queries (indexes, avoiding N+1 queries), which every framework lets you get right or wrong.
- Caching responses or expensive computations.
- Not blocking the event loop in async code; a blocking call inside an
async defFastAPI endpoint can make it slower than a sync Flask app. - Running enough worker processes behind a proper server (Gunicorn, Uvicorn, or Granian).
Pick for fit and productivity; all three are fast enough for the vast majority of applications.
Ecosystem and Community
All three have large ecosystems, but of different shapes.
Django's ecosystem is built around reusable apps that plug into its models and admin: Django REST Framework, Django Ninja, django-allauth for social login, django-filter, Wagtail as a CMS, Celery integrations, and many more. Because every Django project shares the same structure, these apps drop in with little glue.
Flask's ecosystem is extensions: Flask-SQLAlchemy, Flask-Migrate, Flask-WTF, Flask-Login, Flask-Caching. They're generally small and focused. Under the hood, Flask builds on Werkzeug and Jinja, and the same Pallets team maintains Click and MarkupSafe.
FastAPI's ecosystem leans on general Python libraries rather than framework-specific plugins: Pydantic for data, SQLAlchemy or SQLModel for databases, httpx for HTTP clients, and standard OAuth2/JWT libraries for auth. That's a strength (the libraries are useful outside FastAPI too) and a cost (you assemble more yourself).
Which One Should You Choose?
Here's how I'd decide for common situations.
A Content Site, SaaS Product, or Internal Tool with a Database
Django. User accounts, an admin back office, forms, migrations, permissions: you'd need all of these anyway, and Django has them integrated and battle-tested. The admin alone can save weeks on internal tools. Start with getting started with Django.
A JSON API for a Frontend or Mobile App
FastAPI, in most cases. Type-driven validation, automatic OpenAPI docs that frontend developers can use directly, and native async make it the most productive option for pure APIs. See building a REST API with FastAPI.
The exception: if the API also needs an admin interface and a rich relational data model, Django with Django Ninja or DRF gives you both.
A Small Web App, Prototype, or Microservice
Flask or FastAPI. Flask if it serves HTML pages and forms; FastAPI if it's mainly JSON. Flask's single-file style is great for small tools and for learning how web apps work. See building a web app with Flask.
Serving a Machine Learning Model
FastAPI. Pydantic models describe the input features, the docs page doubles as a test console, and async helps when you're calling other services. Keep heavy, CPU-bound inference out of the event loop (use plain def endpoints or a separate worker).
Real-Time Features (WebSockets, Streaming)
FastAPI for a new service. Django with Channels if it's part of an existing Django product.
Your Team Already Knows One
Use that one. Familiarity beats small technical advantages, and all three can be stretched well beyond their sweet spot.
Can You Mix Them?
Yes, and it's common. A typical setup is a Django app for the main product and admin, with a FastAPI service for a high-concurrency API or ML endpoint, sharing a PostgreSQL database or talking over HTTP. Since all three speak standard WSGI or ASGI and are just Python, they deploy the same way, often each in its own container (see deploying a Python web app with Docker).
The thing to avoid is a mid-project rewrite because the first choice didn't fit. A couple of questions answered up front (do I need an admin and user accounts? Is this mainly HTML or mainly JSON? How much concurrency?) usually point clearly to one of them.
Conclusion
Django gives you the most out of the box and is the best fit for database-backed websites and products. Flask gives you the least, which makes it ideal for small apps and for people who want to choose every piece. FastAPI is built for APIs, using type hints to handle validation, serialization, and documentation for you, on an async foundation. Decide based on what you're building rather than benchmarks, and you'll be well served by any of them.


