flask
Flask is a minimal WSGI web framework for Python: routing, request and response handling, Jinja templates, signed-cookie sessions, and a dev server, with everything else (database, auth, forms, admin) left to extensions you choose yourself. It started as a wrapper around Werkzeug and Jinja and became one of the most used Python web frameworks. The 3.x line is maintained by the Pallets organization, and the core design has barely changed in a decade, which is exactly why so much existing Python web code runs on it.
Still the fastest way to stand up a Python HTTP service and a safe, boring default for server-rendered apps and small APIs. For async-heavy or validation-heavy JSON APIs, start with FastAPI instead.
Use it if
- You want a small HTTP service or internal tool running in ten lines with no framework ceremony
- You want to pick your own stack (SQLAlchemy or not, your choice of auth) instead of inheriting one
- You are serving server-rendered HTML with Jinja templates, which ship in the box
- You value hiring and searchability: tutorials, extensions, and Stack Overflow coverage are about as deep as Python gets
- Your workload is concurrency-heavy I/O or websockets: Flask is WSGI and synchronous at heart, and async views still occupy a worker per request; Quart or FastAPI fit better
- You are building a JSON API and want request validation, serialization, and OpenAPI docs generated for you: FastAPI does that out of the box, Flask needs several extensions
- You want batteries included (ORM, admin, auth, migrations): Django ships all of it, while in Flask each one is a separate decision and a third-party extension of varying quality
- You need websockets specifically: WSGI has no native story, so you end up with Flask-SocketIO and its own server constraints, or a different framework
Setup reality
pip install flask brings Werkzeug, Jinja, click, itsdangerous, blinker, and MarkupSafe; Python 3.9+ required. The dev server behind flask run is explicitly not for production, so deployment means choosing gunicorn, waitress, or similar and wiring a reverse proxy yourself (the docs walk through it). Real apps hit the classic pains quickly: circular imports push you into the app-factory plus blueprints layout, sessions silently need a SECRET_KEY, async views need pip install flask[async], and everything beyond routing is an extension choice (Flask-SQLAlchemy, Flask-Login, Flask-Migrate) each with its own docs and release cycle.
Patterns
Minimal apphello-app
# app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello, World!"
# run: flask run (or: flask --app app run)flask run auto-detects app.py or wsgi.py; anything else needs --app. The dev server is not for production.
Typed URL parameters and methodsroute-params
@app.route("/users/<int:user_id>")
def get_user(user_id):
return {"id": user_id}
@app.post("/users")
def create_user():
return {"created": True}, 201Converters (int, float, path, uuid) return 404 on mismatch; app.get/app.post shortcuts exist since Flask 2.0.
Read and return JSONjson-api
from flask import request
@app.post("/items")
def create_item():
data = request.get_json()
return {"created": data["name"]}, 201Returning a dict or list serializes to JSON automatically; get_json() raises 415 when Content-Type is not application/json unless you pass force=True.
Split routes into blueprintsblueprints
from flask import Blueprint
bp = Blueprint("admin", __name__, url_prefix="/admin")
@bp.route("/stats")
def stats():
return {"users": 42}
app.register_blueprint(bp)Blueprint endpoint names are prefixed, so url_for("admin.stats"); changes made to a blueprint after registration are ignored.
Application factoryapp-factory
def create_app(test_config=None):
app = Flask(__name__)
app.config.from_prefixed_env() # reads FLASK_* variables
if test_config:
app.config.update(test_config)
from myapp.routes import bp
app.register_blueprint(bp)
return appThe factory pattern exists to dodge circular imports and to build fresh apps per test; flask run auto-detects a create_app function.
Abort and custom error responseserror-handling
from flask import abort
@app.route("/admin")
def admin():
abort(403)
@app.errorhandler(404)
def not_found(e):
return {"error": "not found"}, 404An errorhandler must return the status code explicitly or the response goes out as 200.
Load configurationconfig
app.config.from_object("myapp.settings.ProdConfig")
app.config.from_prefixed_env()
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024from_object only reads UPPERCASE attributes; from_prefixed_env parses FLASK_-prefixed variables and JSON-decodes their values.
Per-request state with g and teardownrequest-lifecycle
from flask import g, request
@app.before_request
def load_user():
g.user = lookup_user(request.headers.get("Authorization"))
@app.teardown_appcontext
def close_db(exc):
db = g.pop("db", None)
if db is not None:
db.close()g is per-request, not shared state; code outside a request needs an explicit "with app.app_context():" or g raises a context error.
Cookie sessionssessions
import os
from flask import session
app.secret_key = os.environ["SECRET_KEY"]
@app.post("/login")
def login():
session["user_id"] = 123
return {"ok": True}Sessions are signed cookies: tamper-proof but readable by the client and capped by cookie size; use a server-side session extension for sensitive or large data.
Accept file uploads safelyfile-upload
from flask import request
from werkzeug.utils import secure_filename
@app.post("/upload")
def upload():
f = request.files["file"]
f.save("/uploads/" + secure_filename(f.filename))
return {"ok": True}Set MAX_CONTENT_LENGTH or anyone can post you an arbitrarily large body; always run user filenames through secure_filename.
Async viewasync-view
@app.route("/slow")
async def slow():
data = await fetch_remote()
return dataNeeds pip install "flask[async]"; each request still occupies a worker, so this is convenience for awaiting libraries, not FastAPI-style concurrency.
Test with the built-in clienttesting
def test_hello():
app = create_app({"TESTING": True})
client = app.test_client()
res = client.get("/")
assert res.status_code == 200test_client() speaks WSGI directly with no server or ports involved; pair it with the app factory to get a clean app per test.