Qu’est-ce que Flask ?
Flask est un framework web minimaliste pour Python. Il vous fournit un objet d’application, un moyen de mapper des URLs à des fonctions, ainsi qu’un modèle de requête et de réponse. Tout le reste — bases de données, authentification, formulaires, tâches de fond — est une décision que vous prendrez plus tard, généralement en ajoutant une extension.
Cette légèreté est tout l’intérêt du framework. Là où Django a une opinion tranchée sur la manière dont un projet doit être structuré, Flask n’en a pratiquement aucune. Pour une petite API, un outil interne ou un premier projet web, cette liberté est un véritable atout. Vous pouvez parcourir l’intégralité du framework en un après-midi et comprendre chaque ligne de votre propre application.
Flask repose sur deux bibliothèques historiques : Werkzeug pour la plomberie WSGI et Jinja2 pour les templates. Toutes deux sont matures et bien documentées, et savoir qu’elles sont au cœur du système permet d’expliquer la majeure partie du comportement de Flask.
L’application Flask la plus petite
Une application Flask complète peut tenir en quelques lignes.
# app.py
from flask import Flask
app = Flask(__name__)
@app.get("/")
def index():
return "Hello, world"
if __name__ == "__main__":
app.run(debug=True)
Flask(__name__) crée l’application et lui indique où trouver les templates et les fichiers statiques. Le décorateur lie une URL à la fonction située en dessous. app.run() lance le serveur de développement. La condition if __name__ == "__main__" empêche le serveur de démarrer lorsque le module est importé par un test ou un serveur WSGI.
Le routage avec les décorateurs
Le décorateur @app.route est le cœur de Flask. Il permet d’enregistrer une règle d’URL ainsi que la fonction qui y répond.
@app.route("/posts")
def list_posts():
return "All posts"
@app.route("/posts/<int:post_id>")
def get_post(post_id):
return f"Post {post_id}"
Les règles peuvent contenir des parties variables écrites entre chevrons. Flask fait correspondre l’URL, convertit la valeur et la transmet à la fonction sous forme d’argument nommé (keyword argument). Si aucune règle ne correspond, Flask renvoie automatiquement une erreur 404.
Variables d’URL et méthodes HTTP
Les convertisseurs rendent les variables typées avant l’exécution de votre code. Les plus courants sont string (par défaut), int, float, path (qui autorise les slashs) et uuid.
@app.get("/users/<username>")
def profile(username):
return f"Profile for {username}"
@app.get("/files/<path:filepath>")
def serve_file(filepath):
return f"File at {filepath}"
Flask 2.0 a ajouté des raccourcis pour chaque méthode. @app.get, @app.post, @app.put, @app.patch et @app.delete sont plus explicites que de passer une liste methods, et une seule fonction peut répondre à plusieurs méthodes.
@app.route("/posts", methods=["GET", "POST"])
def posts():
if request.method == "POST":
return create_post()
return list_posts()
L’objet request
Les données entrantes se trouvent dans l’objet global request, que Flask alimente pour la requête actuelle. Il s’agit d’un proxy, vous pouvez donc l’importer une seule fois en haut du module sans risque.
from flask import request
@app.get("/search")
def search():
query = request.args.get("q", "")
page = request.args.get("page", 1, type=int)
return {"query": query, "page": page}
@app.post("/posts")
def create_post():
payload = request.get_json()
title = payload["title"]
return {"title": title}, 201
request.args contient les valeurs de la query-string, request.form contient les données des formulaires (posts), request.get_json() analyse le corps JSON, request.files contient les fichiers téléchargés, et request.headers fournit les headers bruts. Comme il s’agit de mappings multi-valeurs, utilisez .get() et définissez des valeurs par défaut plutôt que d’indexer les données aveuglément.
Réponses et JSON
Une vue peut retourner une chaîne de caractères, un dictionnaire (que Flask sérialise en JSON), un tuple de (body, status) ou (body, status, headers), ou un objet Response complet.
from flask import jsonify, make_response
@app.post("/posts")
def create_post():
post = {"id": 1, "title": "Hello"}
return jsonify(post), 201
@app.get("/ping")
def ping():
response = make_response({"pong": True})
response.headers["Cache-Control"] = "no-store"
return response
jsonify est la méthode explicite pour retourner du JSON : elle sérialise les données et définit le type de contenu. Retourner un dictionnaire fonctionne également, mais jsonify est plus clair quant à l’intention et gère davantage de types.
Templates avec Jinja2
render_template charge un fichier depuis le dossier templates/ et le rend avec les données que vous transmettez. Jinja2 échappe les variables par défaut, ce qui protège contre les XSS.
from flask import render_template
@app.get("/posts/<int:post_id>")
def get_post(post_id):
post = {"id": post_id, "title": "Hello", "body": "First post"}
return render_template("post.html", post=post)
<!-- templates/post.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{ post.title }}</title>
</head>
<body>
<h1>{{ post.title }}</h1>
<p>{{ post.body }}</p>
</body>
</html>
Jinja supporte {% extends %} pour l’héritage, {% include %} pour les fragments, {% for %} et {% if %} pour la logique, ainsi que des filtres tels que {{ value|upper }}. Gardez vos templates purement présentations ; si la logique devient trop complexe, déplacez-la dans la vue.
Fichiers statiques
Par défaut, Flask sert tout le contenu du dossier static/ via /static/.... Référencez-le avec url_for, ce qui permet de construire l’URL à partir du nom du point de terminaison.
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
En production, laissez un reverse proxy ou un CDN servir les fichiers statiques plutôt que le processus Python. C’est plus rapide et cela libère l’application pour traiter les requêtes.
Blueprints
Un Blueprint est une collection de routes et de templates pouvant être enregistrés sur une application. C’est ainsi qu’une application Flask composée d’un seul module peut évoluer vers un projet complet sans devenir un chaos.
# blog/views.py
from flask import Blueprint, jsonify
bp = Blueprint("blog", __name__, url_prefix="/blog")
@bp.get("/")
def index():
return jsonify(posts=[])
@bp.get("/<int:post_id>")
def detail(post_id):
return jsonify(id=post_id)
# app.py
from blog.views import bp as blog_bp
app.register_blueprint(blog_bp)
Les Blueprints possèdent leur propre préfixe d’URL, leurs propres templates et fichiers statiques, et un seul blueprint peut être enregistré plusieurs fois avec des préfixes différents. Ils facilitent également les tests et la revue de code car chaque fonctionnalité est autonome.
La fabrique d’application (Application Factory)
Une fois que vous utilisez des blueprints, l’étape suivante logique est la mise en place d’une fonction fabrique (factory function) pour construire l’application. C’est la structure standard pour tout projet dépassant le stade de la simple démo.
# app.py
from flask import Flask
from blog.views import bp as blog_bp
def create_app(config_object="config.Config"):
app = Flask(__name__)
app.config.from_object(config_object)
app.register_blueprint(blog_bp)
return app
# wsgi.py
from app import create_app
app = create_app()
La fabrique rend les tests triviaux : vous construisez une application avec une configuration de test, vous exécutez le client, puis vous la supprimez. Cela permet également d’éviter l’importation d’une application globale partiellement configurée, ce qui est à l’origine de la plupart des problèmes d’importations circulaires dans Flask.
Extensions
Le cœur du framework étant minimal, c’est l’écosystème qui fournit le reste. Quelques extensions couvrent la majorité des besoins des projets :
- Flask-SQLAlchemy intègre l’ORM de SQLAlchemy et gère la session par requête.
- Flask-Migrate encapsule Alembic pour permettre le versionnage des changements de schéma.
- Flask-Login gère les sessions utilisateur et le proxy
current_user. - Flask-WTF ajoute la gestion des formulaires, la protection CSRF et l’upload de fichiers.
- Flask-CORS contrôle les requêtes cross-origin pour les API.
# extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate()
# app.py
from extensions import db, migrate
def create_app():
app = Flask(__name__)
app.config.from_object("config.Config")
db.init_app(app)
migrate.init_app(app, db)
return app
Le pattern init_app permet de créer une extension sans application et de la lier plus tard, ce qui permet de garder la factory propre et l’extension importable par les modèles.
Configuration
La configuration est un simple mapping sur app.config. Chargez-la depuis une classe, un fichier ou l’environnement, et gardez vos secrets hors du contrôle de version.
# config.py
import os
class Config:
SECRET_KEY = os.environ["SECRET_KEY"]
SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
JSON_SORT_KEYS = False
app.config.from_object("config.Config")
app.config.from_prefixed_env()
from_prefixed_env() lit les variables d’environnement de FLASK_*, ce qui est pratique dans les conteneurs. Utilisez différentes classes ou fichiers par environnement, et ne commitez jamais le vrai SECRET_KEY — il sert à signer les sessions et les jetons CSRF.
Gestion des erreurs
Enregistrez des gestionnaires d’erreurs pour renvoyer des réponses cohérentes dans toute l’application. Les gestionnaires peuvent être globaux ou limités à un blueprint.
from flask import jsonify
from werkzeug.exceptions import HTTPException
@app.errorhandler(404)
def not_found(error):
return jsonify(error="not_found"), 404
@app.errorhandler(Exception)
def unhandled(error):
if isinstance(error, HTTPException):
return error
app.logger.exception(error)
return jsonify(error="internal_error"), 500
Le gestionnaire pour HTTPException est important : les erreurs intégrées de Flask sont également des exceptions, et un gestionnaire Exception générique transformerait autrement une erreur 404 en erreur 500.
Tests
Le client de test de Flask effectue des requêtes au sein du même processus, sans serveur ni réseau.
import pytest
from app import create_app
@pytest.fixture
def client():
app = create_app("config.TestConfig")
with app.test_client() as client:
yield client
def test_health(client):
response = client.get("/health")
assert response.status_code == 200
assert response.get_json() == {"status": "ok"}
def test_missing_post(client):
response = client.get("/posts/999")
assert response.status_code == 404
La factory et test_client() fonctionnent ensemble : chaque test reçoit une instance d’application fraîche, ce qui empêche les fuites d’état entre les tests. Surchargez la configuration pour les tests et utilisez une base de données en mémoire lorsque vous en ajouterez une.
WSGI et déploiement
Flask est une application WSGI, ce qui signifie que n’importe quel serveur WSGI peut l’exécuter. Le serveur de développement est réservé exclusivement au développement.
pip install gunicorn
gunicorn --workers 4 --bind 0.0.0.0:8000 "app:create_app()"
En production, utilisez Gunicorn ou uWSGI derrière Nginx, gérez la terminaison TLS au niveau du proxy, et servez les fichiers statiques via le proxy ou un CDN. Configurez DEBUG=False, mettez en place la journalisation (logging) et récupérez vos secrets depuis les variables d’environnement. La conteneurisation de l’application rend le déploiement portable d’un hôte à l’autre.
Bonnes pratiques
- Commencez par un module unique, puis passez à une factory et des blueprints à mesure que le projet s’agrandit.
- Utilisez
url_forpour construire vos URLs au lieu de coder les chemins en dur. - Gardez la configuration dans des classes ou des variables d’environnement, jamais directement dans le code.
- Utilisez
jsonifyet des codes de statut explicites pour les réponses API. - Validez chaque donnée d’entrée avant qu’elle n’atteigne votre logique métier.
- Enregistrez des gestionnaires d’erreurs pour que les échecs retournent un format cohérent.
- Utilisez le client de test et une instance d’application fraîche pour chaque test.
- Ne lancez jamais le serveur de développement en production.
Erreurs courantes
- Laisser
debug=Trueactivé en production, ce qui expose un débogueur interactif. - Importer un
appglobal partout, créant ainsi des imports circulaires. - Lire
request.jsonsans gérer les corps de requête malformés ou manquants. - Écrire les URL en dur dans les templates au lieu d’utiliser
url_for. - Construire un
app.pymonolithique avec des centaines de routes. - Oublier la protection CSRF lors de l’utilisation de formulaires.
- Stocker des secrets dans
config.pyet les commiter. - Supposer que Flask possède un ORM ou un système d’authentification intégré, pour ensuite tenter de recréer les deux maladroitement.
Et après ?
Flask est le moyen le plus rapide de comprendre le fonctionnement réel d’un framework web, car très peu de choses sont masquées. Si vous recherchez un ORM, des migrations, l’authentification et une interface d’administration sans avoir à tout assembler vous-même, consultez le guide Django. Si vous voulez une validation automatique et une documentation API basée sur les type hints, passez à FastAPI. Pour concevoir les endpoints que vous venez de créer, le guide sur les REST APIs est le compagnon idéal. Si vous ajoutez une base de données, la roadmap backend couvre SQL, le langage qui sous-tend l’ORM.