How Django and Flask compare on scope, data access, async support and project structure, with the same endpoint written in each.
Django and Flask have been the two default answers for Python web apps for over a decade. Django decides most things for you, and Flask decides very little. The trade shows up as the app grows, when a Django team is working inside conventions it did not write and a Flask team is maintaining structure it wrote by hand.
The main differences:
A Django model describes a table, and the models documentation covers how fields, relations and managers map to SQL. Migrations come from makemigrations, and the admin and auth system read the same models.
import json
from django.db import models
from django.http import HttpRequest, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
class User(models.Model):
email = models.EmailField(unique=True)
@csrf_exempt
@require_POST
def create_user(request: HttpRequest) -> JsonResponse:
email = json.loads(request.body)["email"]
user = User.objects.create(email=email)
return JsonResponse({"id": user.pk, "email": user.email}, status=201)
The cost is that Django's way is the expected way, so swapping out the ORM or the settings module is possible but works against much of the ecosystem.
Flask's design decisions page explains why the core stays small and why it uses context locals such as request. The same endpoint is a route function that opens a connection with psycopg:
import os
import psycopg
from flask import Flask, request
app = Flask(__name__)
@app.post("/users")
def create_user():
email = request.get_json()["email"]
with psycopg.connect(os.environ["DATABASE_URL"]) as conn:
conn.execute("INSERT INTO users (email) VALUES (%s)", (email,))
return {"email": email}, 201
Larger Flask apps usually adopt Flask-SQLAlchemy for models and blueprints for structure. Each of those is a choice the team makes and documents for itself.
Both run as WSGI apps behind servers such as Gunicorn, following the interface that PEP 3333 defines. Each request holds a worker until it finishes, so throughput scales with worker count. Django can also run under ASGI for async views. For most apps the database and external calls cost more time than either framework does.
Encore combines the framework you already use with infrastructure declared in Python. The infrastructure SDK is installed as encore-dev, and Django or Flask keeps the routes and views. Postgres databases, Pub/Sub topics, object storage buckets, cron jobs and secrets are module-level declarations that Encore discovers and provisions.
The Flask app below writes signups to a declared database and stores avatars in a declared bucket. The bucket API is async, so the synchronous view calls it through async_to_sync:
import psycopg
from asgiref.sync import async_to_sync
from encore.storage import objects, postgres
from flask import Flask, request
db = postgres.Database("users", migrations="./migrations")
avatars = objects.Bucket("avatars")
app = Flask(__name__)
@app.post("/users")
def create_user():
email = request.get_json()["email"]
with psycopg.connect(db.connection_string) as conn:
conn.execute("INSERT INTO users (email) VALUES (%s)", (email,))
return {"email": email}, 201
@app.put("/users/<int:user_id>/avatar")
def upload_avatar(user_id: int):
async_to_sync(avatars.upload)(
f"{user_id}.png", request.get_data(), content_type="image/png"
)
return "", 204
A Django view uses the same pattern for the bucket, and the ORM connects through db.connection_string in settings.py:
from asgiref.sync import async_to_sync
from django.http import HttpRequest, HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from encore.storage import objects
avatars = objects.Bucket("avatars")
@csrf_exempt
@require_http_methods(["PUT"])
def upload_avatar(request: HttpRequest, user_id: int) -> HttpResponse:
async_to_sync(avatars.upload)(
f"{user_id}.png", request.body, content_type="image/png"
)
return HttpResponse(status=204)
encore run starts Postgres locally and serves the bucket through a local emulator, and the CLI reference lists encore test, which runs tests against Encore-managed resources.
A deploy provisions the database and bucket in your own AWS or GCP account.
When a coding agent adds a bucket for exports, the declaration and the view are one change the agent can run locally, and cloud access sits with Encore's IAM role, not the agent. See the AI integration guide for the rules files that describe these conventions to an agent.
Python support is in beta, and you can request access.
Encore works with either one when you want the database and buckets running locally and provisioned in your own cloud account.
Use Django when the app is relational and you want the ORM, migrations, auth and admin to come with the framework. Use Flask for smaller services where you prefer a minimal core and pick each extension yourself.
A first Flask app is shorter, because a single file with a route is a working app. Django asks you to learn projects, apps, settings and the ORM up front, and in return those parts are already decided as the app grows.
No. Most Flask apps use SQLAlchemy through the Flask-SQLAlchemy extension, with Alembic or Flask-Migrate for migrations.
Install the encore-dev package and declare postgres.Database and objects.Bucket as module-level variables. Django and Flask code can connect to the database through db.connection_string, and synchronous views call the async bucket methods with asgiref's async_to_sync. encore run starts Postgres and the bucket locally, and a deploy provisions them in your own AWS or GCP account.
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.