How the three most used Python web frameworks compare on async, validation, data access and project structure, and what each leaves for you to set up before production.
FastAPI, Django and Flask cover most Python web backends, and they sit at different points on the same line, with Flask giving you a small synchronous core, Django giving you a full application with an ORM and admin, and FastAPI giving you an async API layer built on type hints. The consequences of the choice tend to show up later rather than on day one, for example when a Flask app has grown its own validation and structure by hand, when a Django project needs async endpoints that its sync-first extensions do not support, or when a FastAPI codebase has assembled an ORM, migrations and auth from separate libraries that nobody chose together.
The main differences:
Encore is left out of these tables because it is not a fourth framework. It is an infrastructure SDK that works with any Python framework, so the section below shows it added to FastAPI, Django and Flask.
Encore combines your choice of framework with infrastructure declared in Python, as an SDK you install into the existing app, so FastAPI, Django, Flask or any other framework keeps routing and request handling while Postgres databases, Pub/Sub topics, object storage buckets, Redis, cron jobs and secrets are module-level declarations that Encore provisions. encore run starts the databases, topics and buckets locally next to the app, and a deploy creates the same resources in your own AWS or GCP account.

With FastAPI, the database and topic are declarations at the top of the module and the handler queries and publishes through them:
from encore import pubsub
from encore.storage import postgres
from fastapi import FastAPI
from pydantic import BaseModel
db = postgres.Database("users", migrations="./migrations")
signups = pubsub.Topic("signups")
app = FastAPI()
class Signup(BaseModel):
email: str
@app.post("/users", status_code=201)
async def create_user(req: Signup) -> dict:
row = await db.query(
"INSERT INTO users (email) VALUES ($1) RETURNING id", req.email
).one()
await signups.publish({"user_id": row["id"], "email": req.email})
return {"id": row["id"]}
With Django, the ORM, admin and sessions stay as they are and the standard PostgreSQL backend connects to the database Encore provisions, using its connection string in settings.py:
# settings.py; storage/resources.py declares db = postgres.Database("django")
from psycopg.conninfo import conninfo_to_dict
from storage.resources import db
connection = conninfo_to_dict(db.connection_string)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": connection.pop("dbname"),
"USER": connection.pop("user", ""),
"PASSWORD": connection.pop("password", ""),
"HOST": connection.pop("host", ""),
"PORT": connection.pop("port", ""),
"OPTIONS": connection,
}
}
With Flask, which has no lifespan hook to attach to, the SDK works the framework-agnostic way its README describes: the declarations sit at module scope, importing the SDK initializes the Encore runtime when the app runs through Encore, and synchronous views call the async API through asgiref.sync.async_to_sync, the same bridge Encore uses in its Django example. The app binds to the address Encore passes in ENCORE_LISTEN_ADDR, and an encore.service.json next to it declares the service:
import os
from asgiref.sync import async_to_sync
from encore.storage import postgres
from flask import Flask, jsonify
db = postgres.Database("users", migrations="./migrations")
app = Flask(__name__)
async def find_user(user_id: int):
return await db.query("SELECT id, email FROM users WHERE id = $1", user_id).first()
@app.get("/users/<int:user_id>")
def get_user(user_id: int):
row = async_to_sync(find_user)(user_id)
if row is None:
return jsonify(error="not found"), 404
return jsonify(row)
if __name__ == "__main__":
host, _, port = os.environ.get("ENCORE_LISTEN_ADDR", "127.0.0.1:8000").rpartition(":")
app.run(host=host, port=int(port))
Background work that would otherwise need a separate queue becomes a subscription with its retry policy in the declaration, and a cron job declaration replaces a scheduler process by having Encore send a POST to an endpoint on a schedule.
For an AI coding agent, a new queue is then a few lines of Python in the same pull request as the logic, which it can run locally right away instead of waiting for someone to create it in a cloud console.
encore run starts the declared infrastructure locally, and encore test runs tests with Encore-managed resources.async def handlers that receive a typed Message, with retries, backoff and concurrency configured in the declaration.Encore works with any Python framework, with FastAPI, Django and Flask as the examples here. The SDK API is async, so synchronous Django and Flask views call it through asgiref.sync.async_to_sync, and subscriptions start when encore.start() is awaited on the app's event loop, which in FastAPI fits in the lifespan handler. Python support is in beta, and you can sign up for access.
FastAPI reads path, query and body parameters from the function signature, validates them with Pydantic and generates an OpenAPI schema with interactive docs.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Signup(BaseModel):
email: str
@app.post("/users", status_code=201)
async def create_user(req: Signup) -> dict:
return {"email": req.email}
Depends provides dependency injection for auth, sessions and shared logic.FastAPI does not include an ORM, migrations, auth or a project layout, so a growing codebase depends on conventions the team sets, and BackgroundTasks runs in the request process, which makes it unsuitable for work that has to survive a restart.
Django includes an ORM, migrations, admin, authentication, forms and templating, and APIs are usually built with Django REST framework or django-ninja on top.
# views.py, using django-ninja for typed request bodies
from ninja import NinjaAPI, Schema
api = NinjaAPI()
class Signup(Schema):
email: str
@api.post("/users")
def create_user(request, req: Signup):
return {"email": req.email}
Django supports async views and parts of the ORM run async, but many third-party apps are still synchronous, and its conventions add weight when the service only needs a handful of JSON endpoints.
Flask provides routing, request and response objects and templating, and leaves validation, data access and auth to extensions.
from flask import Flask, request
app = Flask(__name__)
@app.post("/users")
def create_user():
data = request.get_json()
return {"email": data["email"]}, 201
Flask is WSGI and synchronous by default, and its async views run each coroutine in an event loop on the worker thread handling the request, which does not give the concurrency of an ASGI server, so I/O-heavy APIs with many concurrent requests are a better fit for FastAPI.
FastAPI runs on an ASGI server such as Uvicorn, so an async handler that awaits a database query frees the worker for other requests in the meantime. Flask and synchronous Django views block a worker thread for the duration of each request, and they scale by adding threads or processes behind Gunicorn. In practice the database and external calls dominate response times for most APIs, so the concurrency model matters most for services with many slow I/O calls, such as ones that call LLM APIs.
Python support in Encore is in beta, and you can sign up for access if you want to try it with your FastAPI or Django app.
Use FastAPI for new JSON APIs that benefit from async handlers and type-hint validation, Django for relational apps that want an ORM, migrations, admin and auth included, and Flask for small services where you want a minimal core and choose each extension yourself.
FastAPI on an ASGI server usually handles more concurrent I/O-bound requests than synchronous Flask or Django views, because handlers can await database and network calls. For most apps the database and external calls dominate response time, so the framework is rarely the bottleneck.
Yes. Most Django APIs use Django REST framework or django-ninja, which adds type-hint-driven schemas similar to FastAPI while keeping the Django ORM and admin.
Usually route by route. The routing decorators look similar, but request parsing moves from request.get_json() to Pydantic models in the signature, and synchronous extensions such as Flask-SQLAlchemy need async or sync replacements that work outside Flask.
Encore is an infrastructure SDK you add to your existing app rather than a web framework, and it works with whichever framework you pick. You keep FastAPI, Django, Flask or any other framework for routing and request handling and declare Postgres databases, Pub/Sub topics, buckets, cron jobs and secrets in Python. encore run starts the databases, topics and buckets locally, cron jobs run only where the app is deployed, and a deploy provisions everything in your own AWS or GCP account. Python support is in beta.
Install the encore-dev package in the app you already have and declare postgres.Database and pubsub.Topic as module-level variables next to your routes. encore run starts them locally and a deploy provisions them in your own AWS or GCP account, and the same SDK works with whichever framework you pick, so a Django app can use it too.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.