flask review
Flask 3.1.3 is a WSGI web framework built around Werkzeug routing and requests, Jinja templates, Click commands, signed cookies, and application or request contexts. It gives you routes and lifecycle hooks without choosing an ORM, validation library, authentication system, or project layout. The current patch changes session access tracking so key-only operations such as `in` and `len()` mark the session as accessed, addressing GHSA-68rp-wp8r-4726. The 3.1 line also added trusted-host checks, multipart limits, per-request body limits, partitioned cookies, and secret-key fallbacks. Our Python 3.12 import succeeded in 0.63 seconds and the distribution shipped `py.typed`.
Our Flask 3.1.3 install finished in 0.4 seconds, occupied 3 MB across 7 packages, imported in 0.63 seconds, and had 0 pip-audit findings. It remains a good fit for conventional WSGI applications that want to pick their own database and validation pieces; async-first APIs or batteries-included business systems should start elsewhere.
We installed it
| Install | ✓ · 0.4s | 7 packages on disk · 3 MB |
| Import | ✓ | import flask in 0.63s · pure Python · py.typed · requires Python >=3.9 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does flask install cleanly?
Yes. In a fresh container with an empty cache, pip install flask finished in 0.4s, leaving 7 packages and 3 MB on disk. pip-audit reported no known vulnerabilities.
What does flask need to run?
Python >=3.9, and nothing compiled: it is pure Python. In our run import flask succeeded in 0.63s, and the package ships py.typed for type checkers.
flask or django: which should you use?
django: Choose it when ORM, admin, forms, authentication, migrations, and a prescribed application layout should arrive together. Our Flask 3.1.3 install finished in 0.4 seconds, occupied 3 MB across 7 packages, imported in 0.63 seconds, and had 0 pip-audit findings.
When should you not use flask?
WebSockets, high fan-out streaming, or long-lived async connections define the product. Flask async views still occupy one WSGI worker; Quart, Starlette, or FastAPI fit an ASGI service better.
Discussed on
- hnFlask 2.0.0 has been merged into master150 points
- hnFlask 2 Coming soon with Async Support7 points
- hnFlask fails after itsdangerous module’s update5 points
Use it if
- A Python service needs conventional HTTP routes, templates, signed sessions, and CLI commands while the team wants to choose its own database and validation layers.
- The deployment stack already speaks WSGI through Gunicorn, uWSGI, mod_wsgi, or another production server.
- A small application should start in one file but retain a documented path to factories, blueprints, extensions, and tests.
- Synchronous request handlers dominate, with occasional awaited I/O that does not require thousands of long-lived connections.
- WebSockets, high fan-out streaming, or long-lived async connections define the product. Flask async views still occupy one WSGI worker; Quart, Starlette, or FastAPI fit an ASGI service better.
- You want request models, OpenAPI generation, dependency injection, and response validation built into the framework. Flask leaves each of those choices to your application or extensions.
- A batteries-included admin, ORM, forms system, and migration workflow are requirements. Django packages those decisions together instead of asking the team to assemble them.
- The team assumes `flask run` is a production process. Pallets labels it a development server; production needs a separate WSGI server, proxy configuration, worker limits, and deployment supervision.
- The project still imports removed 2.x APIs such as `before_first_request`, `app.env`, `json_encoder`, or old JSON config keys. A Flask 3 upgrade requires code changes before the server starts.
Setup reality
We installed Flask 3.1.3 in a clean Python 3.12 Bookworm container. The operation took 0.4 seconds and left 7 packages using 3 MB. pip-audit found 0 known vulnerabilities. Package metadata lists 9 direct dependencies, requires Python 3.9 or newer, and does not state a license there, although GitHub identifies BSD-3-Clause. It is pure Python and includes py.typed. import flask worked in 0.63 seconds in our no-cache sandbox.
A route can run with no credentials, but sessions require a high-entropy SECRET_KEY. Signed cookie contents are readable by the browser, so store identifiers rather than secrets. Flask 3.1 supports SECRET_KEY_FALLBACKS for key rotation, though extensions must implement compatible fallback behavior. Environment loading through the CLI needs the dotenv extra. Factory-based applications should load configuration before registering extensions and blueprints.
The built-in server is for local development. Put a production WSGI server behind a trusted reverse proxy and apply ProxyFix only for the exact proxy hops you operate; accepting forged forwarded headers changes generated URLs and security decisions. Set TRUSTED_HOSTS, MAX_CONTENT_LENGTH, MAX_FORM_MEMORY_SIZE, and MAX_FORM_PARTS where untrusted traffic reaches the app. Flask does not add CSRF protection, authentication, database pooling, or rate limiting by itself.
Installing flask[async] lets an async view await I/O, but each request still consumes one WSGI worker and background tasks spawned by the view are cancelled when its event loop stops. Use a task queue for work that must outlive the response. Request and application globals are context-local, not shared caches. Version 3.1.3 changes which cookie-session reads set Vary: Cookie, so edge caching should never treat a session-touching response as universally reusable.
Patterns
Define a small Flask app create-app
from flask import Flask
app = Flask(__name__)
@app.get("/")
def home():
return {"service": "ready"}Use `flask --app app run` for local work. Pallets does not support the development server as a production process.
Build applications through a factory create-app-factory
from flask import Flask
def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True)
app.config.from_prefixed_env()
if test_config:
app.config.update(test_config)
from .api import bp
app.register_blueprint(bp)
return appLoad configuration before registering extensions and blueprints. Tests can call the factory with isolated settings.
Group routes under a prefix register-blueprint
from flask import Blueprint
bp = Blueprint("orders", __name__, url_prefix="/orders")
@bp.get("/<int:order_id>")
def get_order(order_id):
return {"id": order_id}A blueprint prefixes endpoint names too, so this view is addressed as `orders.get_order` in `url_for()`.
Validate a JSON request read-json
from flask import request
@app.post("/orders")
def create_order():
body = request.get_json()
if not isinstance(body.get("quantity"), int):
return {"error": "quantity must be an integer"}, 400
return {"id": save_order(body)}, 201`get_json()` expects a JSON content type. Flask supplies parsing, while schema validation remains application code or an extension.
Harden a signed cookie session configure-session
import os
from flask import session
app.config.update(
SECRET_KEY=os.environ["FLASK_SECRET_KEY"],
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax",
)
@app.post("/login")
def login():
session.clear()
session["user_id"] = 42
return {"ok": True}The signature prevents undetected changes, but it does not encrypt cookie data. Keep secrets and bulky objects out of the session.
Accept a previous signing key rotate-secret-key
app.config.update(
SECRET_KEY=os.environ["FLASK_SECRET_KEY"],
SECRET_KEY_FALLBACKS=[os.environ["FLASK_OLD_SECRET_KEY"]],
)Flask 3.1 can unsign cookies with fallback keys. Remove an old key after its session lifetime has expired.
Bound multipart request cost limit-uploads
app.config.update(
MAX_CONTENT_LENGTH=8 * 1024 * 1024,
MAX_FORM_MEMORY_SIZE=256 * 1024,
MAX_FORM_PARTS=50,
)Flask 3.1 separates the total body limit, in-memory form field limit, and multipart part count.
Return JSON for HTTP exceptions handle-http-errors
from werkzeug.exceptions import HTTPException
@app.errorhandler(HTTPException)
def http_error(error):
return {
"error": error.name,
"detail": error.description,
}, error.codeReturn the exception status code. A bare dictionary from an error handler would otherwise become a successful response.
Release a context-local database handle close-request-resource
from flask import g
def get_db():
if "db" not in g:
g.db = connect_database()
return g.db
@app.teardown_appcontext
def close_db(error):
db = g.pop("db", None)
if db is not None:
db.close()`g` belongs to the active application context. It is suitable for one request's handle, not cross-request caching.
Keep context during a streamed response stream-response
from flask import Response, stream_with_context
@app.get("/export")
def export():
@stream_with_context
def rows():
yield "id,name\n"
for row in load_rows():
yield f"{row.id},{row.name}\n"
return Response(rows(), mimetype="text/csv")The ordinary request context ends when the view returns. `stream_with_context` extends it through iterator consumption.
Call the app without a socket test-route
def test_create_order(app):
client = app.test_client()
response = client.post("/orders", json={"quantity": 2})
assert response.status_code == 201
assert response.json["id"]The test client invokes WSGI directly. Create a fresh factory instance when a test changes configuration or extension state.
Await I/O in one view write-async-view
@app.get("/upstream")
async def upstream():
result = await fetch_remote_record()
return resultInstall the `async` extra. One async request still occupies one WSGI worker, and child tasks do not survive the response.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| django | PyPI | Choose it when ORM, admin, forms, authentication, migrations, and a prescribed application layout should arrive together. |
| fastapi | PyPI | Choose it for typed request models, generated OpenAPI, dependency injection, and an ASGI runtime. |
| starlette | PyPI | Choose it for a smaller ASGI toolkit with WebSockets and async middleware but without FastAPI's validation layer. |
| quart | PyPI | Choose it when Flask-like APIs are useful but the server must be async-native and support WebSockets. |
More web backend guides
urllib3 · requests · ws · anyio · httpx · undici · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

