Telechargé par Evens Toussaint

Guide complet Flask 2026 : Développer des apps Web avec Python

publicité
Guide complet Flask — Édition 2026
FLASK
Guide complet pour apprendre à développer des applications Web avec
Python
Du premier « Hello World » à une application structurée, testée et déployée
Niveau
Débutant → intermédiaire/avancé
Socle technique
Flask 3.1.x • Python 3.12+ recommandé • SQLAlchemy
2.x
Approche
Explications • exemples • exercices • projet fil rouge
Édition
Septembre 2026
Manuel autonome d’apprentissage et de référence
Page 1
Guide complet Flask — Édition 2026
Avant-propos
Flask est un framework Web Python léger, fondé sur WSGI. Sa philosophie consiste à fournir un noyau simple — routage,
requêtes/réponses, sessions, templates, CLI — puis à laisser l’application choisir les extensions adaptées à ses besoins.
Cette souplesse rend Flask excellent pour apprendre le développement Web, créer des API, des outils internes et des
applications métier, mais elle impose aussi de comprendre l’architecture plutôt que de recopier des recettes.
Ce document est conçu comme un parcours complet. Les premiers chapitres expliquent les mécanismes fondamentaux du
Web et de Flask. Les chapitres intermédiaires introduisent Jinja, les formulaires, la persistance des données,
l’authentification et la modularisation. Les derniers chapitres couvrent les tests, la sécurité, le déploiement, les
performances, le débogage et les choix d’architecture. Un projet fil rouge rassemble ensuite ces notions dans une
application structurée.
Référence de version — Au moment de cette édition, la documentation officielle courante est celle de Flask 3.1.x.
Flask prend en charge Python 3.9 et plus, mais ce guide recommande Python 3.12 ou plus récent afin de travailler avec
un environnement moderne et compatible avec les extensions récentes.
Principe pédagogique — Ne cherchez pas à mémoriser l’API. Cherchez à comprendre le cycle requête → routage →
fonction de vue → accès aux données → rendu/réponse. Une fois ce modèle mental acquis, l’essentiel de Flask devient
prévisible.
Comment utiliser ce manuel
1.
2.
3.
4.
5.
Lire et exécuter. Tapez réellement les exemples dans un environnement virtuel. La lecture seule donne une fausse
impression de maîtrise.
Modifier les exemples. Changez une route, un paramètre, un template ou une requête SQL et observez les
conséquences.
Faire les exercices. Les exercices courts servent à transformer une compréhension passive en compétence
opérationnelle.
Construire le projet fil rouge. À partir du chapitre dédié, assemblez une application modulaire plutôt que d’empiler
du code dans un seul fichier.
Revenir aux fiches mémo. Les annexes regroupent les commandes, objets Flask et erreurs fréquentes pour un usage
quotidien.
Prérequis




Python : variables, fonctions, modules, classes, exceptions, listes/dictionnaires, importations.
HTML : structure générale d’une page, formulaires, liens, attributs.
Notions utiles mais non obligatoires : SQL, Git, ligne de commande, HTTP.
Un éditeur : VS Code, PyCharm ou équivalent.
Page 2
Guide complet Flask — Édition 2026
Table des matières






























1. Comprendre le Web et la place de Flask
2. Installer l’environnement de travail
3. Votre première application Flask
4. Routage, URLs et paramètres
5. Requêtes HTTP, réponses et cycle de traitement
6. Templates Jinja et héritage de pages
7. Fichiers statiques, CSS et organisation de l’interface
8. Formulaires, validation et protection CSRF
9. Configuration, variables d’environnement et secrets
10. Contextes Flask : application, requête, session et g
11. Base de données avec Flask-SQLAlchemy
12. CRUD moderne avec SQLAlchemy 2.x
13. Relations entre modèles et requêtes avancées
14. Migrations de base de données
15. Authentification et gestion des mots de passe
16. Autorisation, rôles et contrôle d’accès
17. Blueprints : modulariser l’application
18. Application Factory et extensions
19. Construire une API JSON avec Flask
20. Uploads, fichiers et contenu utilisateur
21. Erreurs, journalisation et observabilité
22. Tester une application Flask avec pytest
23. Sécuriser une application Flask
24. Performance, tâches longues et async
25. Déployer Flask en production
26. Projet fil rouge : TaskFlow
27. Débogage : erreurs fréquentes et méthode de diagnostic
28. Bonnes pratiques d’architecture et revue de code
29. Exercices de synthèse et pistes de correction
Annexes : commandes, objets, glossaire et références officielles
Astuce Word : les titres sont structurés avec les styles Titre 1, Titre 2 et Titre 3. Le volet Navigation de Word permet donc
de parcourir rapidement le document.
Page 3
Guide complet Flask — Édition 2026
1. Comprendre le Web et la place de Flask
1.1 Le modèle client–serveur
Une application Web est un programme serveur qui reçoit des requêtes HTTP et renvoie des réponses. Le navigateur est un
client : il demande une ressource, par exemple GET /tasks/42. Le serveur choisit le code à exécuter, éventuellement
interroge une base de données, puis renvoie une réponse HTML, JSON, un fichier ou un code d’erreur.
Schéma mental minimal
Navigateur / client
| requête HTTP
v
Serveur Web / WSGI server
|
v
Application Flask
| routes | logique | données | templates |
|
v
Réponse HTTP : HTML / JSON / fichier / redirection
1.2 HTTP en cinq notions
Notion
Exemple
À retenir
Méthode
GET, POST, PUT, PATCH, DELETE
Décrit l’intention de la requête.
URL
/users/18
Identifie une ressource ou une action.
En-têtes
Content-Type, Authorization
Métadonnées de la requête ou de la réponse.
Corps
JSON, formulaire, fichier
Contenu envoyé au serveur.
Statut
200, 201, 400, 404, 500
Résultat standardisé du traitement.
1.3 Flask, WSGI, Werkzeug, Jinja et Click
Flask est une application WSGI. WSGI définit l’interface entre un serveur Web Python et l’application. Werkzeug fournit
notamment le routage et les objets de requête/réponse. Jinja génère du HTML à partir de templates. Click alimente la
commande flask et les commandes CLI personnalisées. Flask utilise aussi MarkupSafe, ItsDangerous et Blinker pour
l’échappement, la signature de données et les signaux.
Microframework ne veut pas dire “petit projet” — Le terme signifie surtout que Flask n’impose pas une couche
ORM, un moteur de formulaires ou un système d’authentification unique. Une application Flask peut devenir très
grande si elle est correctement structurée.
1.4 Quand choisir Flask ?



Bon choix : API REST, outils internes, portails métier, prototypes sérieux, services Web, backends de taille petite à
importante.
Moins naturel : applications principalement WebSocket/ASGI ou très fortement asynchrones ; un framework asyncfirst peut alors être plus approprié.
Décision d’architecture : Flask donne de la liberté. Cette liberté implique de définir tôt les conventions de structure,
configuration, sécurité et tests.
Exercice — Lire une requête — Pour GET /products/12?currency=USD, identifiez la méthode, le chemin, le
paramètre de chemin éventuel et le paramètre de query string.
Piste de correction : Méthode GET ; chemin /products/12 ; “12” peut être capturé par une route dynamique ; query string
currency=USD.
Page 4
Guide complet Flask — Édition 2026
Page 5
Guide complet Flask — Édition 2026
2. Installer l’environnement de travail
2.1 Pourquoi un environnement virtuel ?
Un environnement virtuel isole les dépendances d’un projet. Deux applications peuvent ainsi utiliser des versions
différentes d’une bibliothèque sans conflit. Il faut créer un environnement par projet et l’activer avant d’installer Flask.
Windows (PowerShell ou terminal)
mkdir mon_projet_flask
cd mon_projet_flask
py -3 -m venv .venv
.venv\Scripts\activate
python -m pip install --upgrade pip
pip install Flask
macOS / Linux
mkdir mon_projet_flask
cd mon_projet_flask
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install Flask
2.2 Vérifier l’installation
Commandes de contrôle
python --version
flask --version
pip show Flask
pip list
Nom de fichier à éviter — N’appelez pas votre fichier flask.py : il masquerait le paquet Flask lors de l’import et
provoquerait des erreurs déroutantes.
2.3 Dépendances reproductibles
Un projet professionnel doit expliciter ses dépendances. Pour un petit projet, requirements.txt reste pratique. Pour un
packaging moderne, pyproject.toml est préférable. Le principe essentiel est que l’environnement d’un autre développeur ou
du serveur puisse être reconstruit.
Approche simple
pip freeze > requirements.txt
# Recréer ailleurs
pip install -r requirements.txt
Page 6
Guide complet Flask — Édition 2026
Exemple de pyproject.toml
[project]
name = "taskflow"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"Flask>=3.1,<3.2",
"Flask-SQLAlchemy>=3.1,<3.2",
"Flask-Migrate>=4,<5",
"Flask-Login>=0.7,<0.8",
"Flask-WTF>=1.3,<1.4",
]
[project.optional-dependencies]
dev = ["pytest", "coverage"]
2.4 Arborescence minimale
Au début, restez simple
mon_projet_flask/
├── .venv/
├── app.py
├── templates/
└── static/
Exercice — Créer l’environnement — Créez un dossier flask_lab, un environnement .venv, installez Flask et vérifiez
que “flask --version” fonctionne.
Page 7
Guide complet Flask — Édition 2026
3. Votre première application Flask
3.1 L’application minimale
app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Bonjour Flask !"
L’objet app est l’instance centrale de Flask. Le décorateur @app.route('/') enregistre une règle d’URL. Lorsque Flask reçoit
une requête correspondant à '/', il appelle index(). La valeur retournée est transformée en réponse HTTP.
3.2 Démarrer le serveur de développement
Terminal
flask --app app run --debug
L’option --debug active notamment le reloader et le débogueur. Elle est utile localement, mais ne doit jamais être utilisée
sur un serveur exposé en production.
3.3 Découverte automatique de l’application
Si le fichier s’appelle app.py ou wsgi.py, la commande flask peut souvent découvrir l’application automatiquement. Dans
un projet structuré avec une factory create_app(), Flask sait également détecter cette factory dans les cas usuels.
3.4 Retourner autre chose qu’une chaîne
Plusieurs formes de réponse
from flask import jsonify, redirect, url_for
@app.get("/api/status")
def status():
return {"status": "ok"}
# Flask produit du JSON
@app.get("/old")
def old():
return redirect(url_for("index"))
@app.get("/created")
def created():
return {"id": 10}, 201
Règle de conception — Une fonction de vue doit orchestrer le traitement d’une requête. Évitez d’y accumuler toute la
logique métier, les requêtes SQL, la validation et le formatage : cette dette devient vite difficile à tester.
Exercice — Trois routes — Créez /, /about et /api/status. La dernière doit renvoyer un objet JSON contenant une clé
application et une clé version.
Page 8
Guide complet Flask — Édition 2026
4. Routage, URLs et paramètres
4.1 Routes dynamiques
Paramètres de chemin
@app.get("/users/<username>")
def profile(username):
return f"Profil de {username}"
@app.get("/orders/<int:order_id>")
def order(order_id):
return f"Commande {order_id}"
Les convertisseurs de route valident et transforment les segments. Les principaux sont string, int, float, path et uuid. Utiliser
un convertisseur réduit les validations triviales dans la fonction de vue et évite certaines routes ambiguës.
Convertisseur
Exemple
Valeur reçue
string
/user/<name>
Chaîne sans slash
int
/item/<int:id>
Entier positif
float
/rate/<float:value>
Nombre flottant
path
/files/<path:name>
Chaîne pouvant contenir /
uuid
/objects/<uuid:id>
Objet UUID
4.2 Générer les URLs avec url_for()
Ne concaténez pas vos URLs à la main
from flask import url_for
url_for("profile", username="alice")
# -> /users/alice
url_for() cible un endpoint, généralement le nom de la fonction de vue. Cette indirection protège l’application : si l’URL
change mais que l’endpoint reste le même, les appels à url_for() continuent de fonctionner.
4.3 GET, POST et décorateurs spécialisés
Séparer les intentions HTTP
@app.get("/login")
def login_form():
...
@app.post("/login")
def login_submit():
...
4.4 Slash final et canonicalisation
Une route définie avec un slash final, par exemple /projects/, se comporte comme un dossier et Flask peut rediriger /projects
vers /projects/. Une route sans slash final ne doit pas être supposée accepter automatiquement la variante avec slash.
Exercice — Routage typé — Créez /articles/<int:id> et /files/<path:name>. Testez une valeur entière, une valeur non
entière et un chemin contenant plusieurs niveaux.
Page 9
Guide complet Flask — Édition 2026
5. Requêtes HTTP, réponses et cycle de traitement
5.1 Lire la requête
Query string
from flask import request
@app.get("/search")
def search():
term = request.args.get("q", "")
page = request.args.get("page", 1, type=int)
return {"q": term, "page": page}
Formulaires et fichiers
@app.post("/profile")
def update_profile():
display_name = request.form.get("display_name", "").strip()
avatar = request.files.get("avatar")
return "OK"
JSON
@app.post("/api/tasks")
def create_task():
payload = request.get_json()
title = payload.get("title", "").strip()
return {"title": title}, 201
5.2 Construire une réponse explicitement
Objet Response
from flask import make_response
@app.get("/report")
def report():
response = make_response("rapport")
response.headers["X-App-Version"] = "1.0"
return response
5.3 Codes de statut essentiels
Code
Sens courant
Exemple
200
Succès
GET réussi
201
Créé
POST ayant créé une ressource
204
Pas de contenu
DELETE réussi sans corps
302/303
Redirection
POST puis redirect
400
Requête invalide
Données mal formées
401
Non authentifié
Connexion requise
403
Interdit
Authentifié mais non autorisé
404
Introuvable
Ressource absente
409
Conflit
Doublon / état incompatible
422
Contenu non traitable
Validation sémantique
500
Erreur serveur
Exception non gérée
Page 10
Guide complet Flask — Édition 2026
5.4 Pattern POST/Redirect/GET
Après un formulaire POST réussi, redirigez vers une page GET. Cette technique empêche la resoumission involontaire du
formulaire lors d’un rafraîchissement et sépare proprement l’action de la consultation.
POST/Redirect/GET
@app.post("/notes")
def create_note():
# valider puis enregistrer
...
return redirect(url_for("notes_list"))
5.5 Hooks de requête
Avant, après et teardown
@app.before_request
def before():
...
@app.after_request
def after(response):
response.headers["X-Content-Type-Options"] = "nosniff"
return response
@app.teardown_request
def teardown(error=None):
...
Exercice — API de calcul — Créez GET /sum?a=4&b=7. Convertissez les paramètres en nombres, renvoyez un
JSON avec result, et un statut 400 si une valeur est absente ou invalide.
Page 11
Guide complet Flask — Édition 2026
6. Templates Jinja et héritage de pages
6.1 Pourquoi des templates ?
Écrire du HTML directement dans les fonctions Python devient vite illisible. Jinja sépare la présentation du code serveur :
la vue prépare les données, puis render_template() charge un fichier HTML et lui transmet ces données.
Vue Flask
from flask import render_template
@app.get("/hello/<name>")
def hello(name):
return render_template("hello.html", name=name)
templates/hello.html
<!doctype html>
<html lang="fr">
<head><meta charset="utf-8"><title>Bonjour</title></head>
<body>
<h1>Bonjour {{ name }}</h1>
</body>
</html>
6.2 Variables, conditions et boucles
Syntaxe Jinja
{% if tasks %}
<ul>
{% for task in tasks %}
<li>{{ task.title }}</li>
{% endfor %}
</ul>
{% else %}
<p>Aucune tâche.</p>
{% endif %}
6.3 Héritage de templates
templates/base.html
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>{% block title %}TaskFlow{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
</head>
<body>
<nav>...</nav>
<main>{% block content %}{% endblock %}</main>
</body>
</html>
templates/tasks/index.html
{% extends "base.html" %}
{% block title %}Mes tâches{% endblock %}
{% block content %}
<h1>Mes tâches</h1>
...
{% endblock %}
Page 12
Guide complet Flask — Édition 2026
6.4 Auto-escaping et XSS
Jinja échappe automatiquement les variables dans les templates HTML usuels. Une valeur comme <script> n’est donc pas
exécutée par le navigateur lorsqu’elle est affichée avec {{ variable }}. Évitez le filtre |safe sur du contenu utilisateur non
assaini : vous désactiveriez cette protection.
6.5 Filtres et macros
Quelques filtres / expressions
{{ user.email|lower }}
{{ task.created_at.strftime("%d/%m/%Y") }}
{{ description|default("Aucune description") }}
Séparation des responsabilités — Une règle métier importante ne doit pas vivre uniquement dans un template. Jinja
sert à présenter et à effectuer des décisions d’affichage simples, pas à devenir une seconde couche de logique métier.
Exercice — Page de catalogue — Passez une liste de dictionnaires products au template. Affichez le nom et le prix
dans une boucle, puis un message “Catalogue vide” si la liste est vide.
Page 13
Guide complet Flask — Édition 2026
7. Fichiers statiques, CSS et organisation de l’interface
7.1 Dossier static
Par convention, Flask expose le dossier static sous l’endpoint static. Les feuilles de style, images, scripts JavaScript et
icônes qui ne sont pas générés dynamiquement y sont placés.
Organisation recommandée
app/
├── static/
│
├── css/app.css
│
├── js/app.js
│
└── img/logo.svg
└── templates/
Référencer les fichiers statiques
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
<script defer src="{{ url_for('static', filename='js/app.js') }}"></script>
7.2 Garder l’interface simple au début
Pour apprendre Flask, un CSS léger est préférable à une dépendance UI lourde. Vous devez pouvoir distinguer les
problèmes de serveur, de template et de navigateur. Une fois la mécanique maîtrisée, Bootstrap, Tailwind ou un design
system peuvent être ajoutés sans changer les fondamentaux Flask.
7.3 Messages flash
Côté Python
from flask import flash
flash("Tâche créée.", "success")
Dans base.html
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="flash flash-{{ category }}">{{ message }}</div>
{% endfor %}
{% endwith %}
Exercice — Interface minimale — Créez base.html avec une barre de navigation et un fichier static/css/app.css.
Toutes les pages suivantes doivent hériter de base.html.
Page 14
Guide complet Flask — Édition 2026
8. Formulaires, validation et protection CSRF
8.1 Formulaire HTML simple
HTML natif
<form method="post">
<label for="title">Titre</label>
<input id="title" name="title" required>
<button type="submit">Créer</button>
</form>
Validation côté serveur obligatoire
@app.post("/tasks/new")
def task_create():
title = request.form.get("title", "").strip()
if not title:
flash("Le titre est obligatoire.", "error")
return redirect(url_for("task_new"))
...
La validation HTML du navigateur améliore l’expérience utilisateur, mais elle n’est jamais une barrière de sécurité : un
client peut envoyer directement une requête HTTP. Les contraintes doivent être vérifiées côté serveur.
8.2 Flask-WTF et WTForms
Installation
pip install Flask-WTF
forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Length
class TaskForm(FlaskForm):
title = StringField("Titre", validators=[DataRequired(), Length(max=120)])
submit = SubmitField("Enregistrer")
Vue
@bp.route("/new", methods=["GET", "POST"])
def create():
form = TaskForm()
if form.validate_on_submit():
...
return redirect(url_for("tasks.index"))
return render_template("tasks/form.html", form=form)
Template
<form method="post">
{{ form.hidden_tag() }}
{{ form.title.label }}
{{ form.title() }}
{% for error in form.title.errors %}<span>{{ error }}</span>{% endfor %}
{{ form.submit() }}
</form>
8.3 CSRF
Une attaque CSRF tente d’amener le navigateur d’un utilisateur authentifié à envoyer une requête indésirable vers votre
application. Flask-WTF peut générer et valider un jeton CSRF pour les formulaires. Une SECRET_KEY forte est
nécessaire, car la sécurité de plusieurs mécanismes Flask en dépend.
Page 15
Guide complet Flask — Édition 2026
Version récente — Flask-WTF 1.3.0 a été publié en 2026 et a cessé de prendre en charge Python 3.9. Un
environnement Python moderne évite les incompatibilités inutiles.
Exercice — Formulaire robuste — Créez un formulaire TaskForm avec title obligatoire (120 caractères max) et
description facultative (1000 caractères max). Affichez les erreurs près de chaque champ.
Page 16
Guide complet Flask — Édition 2026
9. Configuration, variables d’environnement et secrets
9.1 app.config
Configuration minimale
app.config.from_mapping(
SECRET_KEY="dev",
SQLALCHEMY_DATABASE_URI="sqlite:///taskflow.db",
)
La configuration est un dictionnaire spécialisé. Les clés de configuration sont généralement écrites en majuscules. Il faut
distinguer les valeurs communes au code des valeurs dépendant de l’environnement : secrets, URI de base de données, clés
d’API, options de sécurité et paramètres de déploiement.
9.2 Ne pas coder les secrets en dur
Variables d’environnement
import os
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"DATABASE_URL", "sqlite:///taskflow.db"
)
9.3 Classes de configuration
Une option possible
class BaseConfig:
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(BaseConfig):
DEBUG = True
class ProductionConfig(BaseConfig):
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
9.4 Instance folder
Le dossier instance est prévu pour les données locales qui ne doivent pas nécessairement être versionnées avec le code :
fichier SQLite local, configuration locale, etc. Il est particulièrement utile pour les petites applications et le développement.
Exercice — Séparer les environnements — Créez une configuration de test utilisant sqlite:///:memory: et
TESTING=True, sans modifier la configuration de développement.
Page 17
Guide complet Flask — Édition 2026
10. Contextes Flask : application, requête, session et g
10.1 Pourquoi des proxys de contexte ?
Dans une vue, request et current_app semblent être des variables globales, mais Flask les résout vers les objets associés au
contexte courant. Cela évite de passer l’objet requête ou l’application à chaque fonction tout en conservant l’isolation entre
requêtes.
Objet
Portée
Usage
current_app
Contexte d’application
Accéder à l’application et sa configuration.
g
Contexte d’application
Stockage temporaire pour une requête /
activité contextuelle.
request
Contexte de requête
Méthode, URL, headers, corps, fichiers, etc.
session
Contexte de requête
Données signées persistées côté client par
défaut.
10.2 Application context manuel
Hors requête HTTP
with app.app_context():
db.create_all()
# current_app et db sont accessibles ici
10.3 g pour une ressource par requête
Cache contextuel
from flask import g
def get_service():
if "service" not in g:
g.service = ExpensiveService()
return g.service
10.4 Session Flask
Session
from flask import session
session["user_id"] = 42
user_id = session.get("user_id")
session.clear()
Attention — La session Flask par défaut est stockée dans un cookie signé, pas chiffré. Ne placez jamais de secrets,
mots de passe ou données sensibles en clair dans la session sous prétexte qu’elle est “côté serveur”.
Exercice — Comprendre le contexte — Écrivez une fonction CLI ou un bloc Python qui tente d’utiliser current_app
hors contexte, puis corrigez-le avec app.app_context().
Page 18
Guide complet Flask — Édition 2026
11. Base de données avec Flask-SQLAlchemy
11.1 Installer et initialiser
Installation
pip install Flask-SQLAlchemy
extensions.py
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
db = SQLAlchemy(model_class=Base)
Dans la factory
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///taskflow.db"
db.init_app(app)
11.2 Définir un modèle SQLAlchemy 2.x
models.py
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
class User(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
name: Mapped[str] = mapped_column(String(120))
Mapped et mapped_column correspondent au style typé moderne de SQLAlchemy 2.x. Les annotations indiquent
clairement les types Python et facilitent les outils d’analyse statique.
11.3 Créer les tables : développement seulement
Création initiale
with app.app_context():
db.create_all()
Important — create_all() crée les tables manquantes mais ne transforme pas un schéma existant lorsque vos modèles
changent. Dès que le projet évolue, utilisez des migrations.
11.4 Session et transaction
Insertion
user = User(email="[email protected]", name="Alice")
db.session.add(user)
db.session.commit()
La session SQLAlchemy suit les objets et groupe les modifications dans une transaction. Après une erreur de base de
données pendant un commit, un rollback est généralement nécessaire avant de réutiliser la session dans le même flux.
Exercice — Premier modèle — Créez un modèle Category avec id et name unique. Insérez deux catégories depuis un
shell Flask.
Page 19
Guide complet Flask — Édition 2026
12. CRUD moderne avec SQLAlchemy 2.x
12.1 Create
Créer
task = Task(title="Lire Flask", done=False, user_id=current_user.id)
db.session.add(task)
db.session.commit()
12.2 Read
Lire plusieurs / un
tasks = db.session.execute(
db.select(Task).order_by(Task.created_at.desc())
).scalars().all()
task = db.get_or_404(Task, task_id)
La propriété Model.query est une interface historique. Dans les nouveaux projets SQLAlchemy 2.x, préférez db.select(...),
db.session.execute(...) et les helpers get_or_404 / first_or_404 adaptés aux vues.
12.3 Update
Modifier
task = db.get_or_404(Task, task_id)
task.title = form.title.data
db.session.commit()
12.4 Delete
Supprimer
task = db.get_or_404(Task, task_id)
db.session.delete(task)
db.session.commit()
12.5 Pagination
Paginer
stmt = db.select(Task).order_by(Task.created_at.desc())
page = db.paginate(stmt, page=page_number, per_page=20, error_out=False)
for task in page.items:
...
12.6 Validation d’unicité et erreurs concurrentes
Vérifier qu’un email n’existe pas avant l’insertion améliore le message utilisateur, mais seule une contrainte UNIQUE dans
la base protège réellement contre une course entre deux requêtes. Une bonne application combine validation applicative et
contraintes de base de données.
Exercice — CRUD complet — Implémentez list/create/edit/delete pour Category. Après chaque POST réussi, utilisez
une redirection. Protégez la suppression contre un identifiant inexistant.
Page 20
Guide complet Flask — Édition 2026
13. Relations entre modèles et requêtes avancées
13.1 One-to-many : User → Task
Relation bidirectionnelle
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
class User(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
tasks: Mapped[list["Task"]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)
class Task(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), index=True)
user: Mapped["User"] = relationship(back_populates="tasks")
13.2 Filtrer
where + order_by
stmt = (
db.select(Task)
.where(Task.user_id == current_user.id, Task.done.is_(False))
.order_by(Task.created_at.desc())
)
tasks = db.session.execute(stmt).scalars().all()
13.3 Jointures
Join via la relation
stmt = (
db.select(Task)
.join(Task.user)
.where(User.email.endswith("@example.com"))
)
13.4 N+1 et chargement des relations
Une boucle qui charge paresseusement une relation pour chaque ligne peut déclencher un grand nombre de requêtes. Pour
les pages de liste, examinez le SQL produit et utilisez les stratégies de chargement SQLAlchemy appropriées, par exemple
selectinload(), lorsque les relations doivent être préchargées.
Précharger une collection
from sqlalchemy.orm import selectinload
stmt = db.select(User).options(selectinload(User.tasks))
Exercice — Relation Category–Task — Ajoutez category_id à Task, créez la relation Category.tasks, puis affichez le
nom de la catégorie dans la liste des tâches.
Page 21
Guide complet Flask — Édition 2026
14. Migrations de base de données
14.1 Pourquoi les migrations ?
Un schéma de base de données change avec le code : nouvelles colonnes, contraintes, tables, index, relations. Une
migration versionne ces changements pour que les environnements de développement, test et production puissent évoluer
de façon contrôlée.
14.2 Flask-Migrate
Installation
pip install Flask-Migrate
extensions.py
from flask_migrate import Migrate
migrate = Migrate()
Dans create_app()
migrate.init_app(app, db)
Workflow initial
flask db init
flask db migrate -m "create user and task"
flask db upgrade
14.3 Modifier un modèle
Cycle normal
# 1. Modifier models.py
# 2. Générer
flask db migrate -m "add task priority"
# 3. RELIRE le script généré
# 4. Appliquer
flask db upgrade
Autogenerate n’est pas un oracle — Alembic compare les métadonnées du modèle et le schéma, mais certaines
opérations nécessitent une revue ou une modification manuelle. Relisez toujours la migration avant de l’exécuter en
production.
14.4 Downgrade et sauvegardes
Une fonction downgrade n’implique pas qu’un retour arrière soit sans risque. Supprimer une colonne peut supprimer des
données. En production, une stratégie de rollback doit considérer code, schéma, données, sauvegarde et compatibilité entre
versions.
Exercice — Migration réelle — Ajoutez une colonne priority à Task avec une valeur par défaut appropriée. Générez
la migration, inspectez upgrade()/downgrade(), puis appliquez-la.
Page 22
Guide complet Flask — Édition 2026
15. Authentification et gestion des mots de passe
15.1 Ne jamais stocker le mot de passe
Une application stocke un hash de mot de passe, pas le mot de passe en clair. Werkzeug fournit des fonctions de hachage
adaptées à l’usage courant. Le modèle peut encapsuler ces opérations pour éviter les erreurs de manipulation.
Modèle User
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
...
password_hash: Mapped[str] = mapped_column(String(255))
def set_password(self, password: str) -> None:
self.password_hash = generate_password_hash(password)
def check_password(self, password: str) -> bool:
return check_password_hash(self.password_hash, password)
15.2 Flask-Login
Installation
pip install Flask-Login
extensions.py
from flask_login import LoginManager
login_manager = LoginManager()
login_manager.login_view = "auth.login"
Initialisation et user loader
login_manager.init_app(app)
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))
Connexion
from flask_login import login_user, logout_user, login_required, current_user
if user and user.check_password(form.password.data):
login_user(user, remember=form.remember.data)
return redirect(url_for("main.index"))
Déconnexion
@bp.post("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for("main.index"))
15.3 Redirection next
Après une redirection vers la page de connexion, Flask-Login peut transporter une destination next. Avant de rediriger vers
une URL fournie par l’utilisateur, vérifiez qu’elle est locale/sûre afin d’éviter une open redirect.
Page 23
Guide complet Flask — Édition 2026
15.4 Réinitialisation de mot de passe
Un flux de récupération robuste utilise un jeton à durée limitée, à usage contrôlé, envoyé vers un canal vérifié. Ne
construisez pas un lien contenant directement l’identifiant et un secret permanent. ItsDangerous peut être utilisé pour signer
des données temporelles, mais le flux complet doit être conçu avec soin.
Exercice — Connexion — Ajoutez une page de connexion utilisant Flask-Login et un formulaire CSRF. Bloquez une
route /dashboard avec @login_required.
Page 24
Guide complet Flask — Édition 2026
16. Autorisation, rôles et contrôle d’accès
16.1 Authentification ≠ autorisation
L’authentification répond à “qui êtes-vous ?”. L’autorisation répond à “avez-vous le droit d’effectuer cette action ?”. Un
utilisateur connecté ne doit jamais pouvoir modifier une ressource qui appartient à un autre utilisateur simplement en
changeant un identifiant dans l’URL.
Contrôle d’objet
@bp.post("/<int:task_id>/delete")
@login_required
def delete(task_id):
task = db.get_or_404(Task, task_id)
if task.user_id != current_user.id:
abort(403)
db.session.delete(task)
db.session.commit()
return redirect(url_for("tasks.index"))
16.2 Décorateur de rôle
Exemple simple
from functools import wraps
from flask import abort
from flask_login import current_user
def role_required(role):
def decorator(view):
@wraps(view)
def wrapped(*args, **kwargs):
if not current_user.is_authenticated:
abort(401)
if current_user.role != role:
abort(403)
return view(*args, **kwargs)
return wrapped
return decorator
16.3 RBAC et permissions
Pour un projet métier, un simple champ role peut devenir insuffisant. Le RBAC (Role-Based Access Control) associe les
permissions à des rôles, puis les rôles aux utilisateurs. Dans les cas complexes, pensez en termes de permission atomique —
task.read, task.update, user.manage — plutôt que de disperser des conditions sur le nom du rôle.
Exercice — Empêcher l’IDOR — Ajoutez une route d’édition de Task et vérifiez explicitement que la tâche
appartient à current_user. Testez qu’un autre utilisateur reçoit 403.
Page 25
Guide complet Flask — Édition 2026
17. Blueprints : modulariser l’application
17.1 Pourquoi les Blueprints ?
Un Blueprint regroupe des routes, templates, fonctions et ressources appartenant à une même composante. Il ne s’agit pas
d’une application autonome, mais d’un ensemble d’opérations que Flask enregistrera sur l’application principale.
app/tasks/routes.py
from flask import Blueprint
bp = Blueprint("tasks", __name__, url_prefix="/tasks")
@bp.get("/")
def index():
...
Enregistrement
from .tasks.routes import bp as tasks_bp
app.register_blueprint(tasks_bp)
17.2 Endpoints qualifiés
Dans un blueprint nommé tasks, la fonction index a généralement pour endpoint tasks.index. Utilisez url_for('tasks.index')
plutôt que de supposer un chemin fixe.
17.3 Structure modulaire
Exemple
app/
├── __init__.py
├── extensions.py
├── models.py
├── auth/
│
├── __init__.py
│
├── routes.py
│
├── forms.py
│
└── templates/auth/
├── tasks/
│
├── __init__.py
│
├── routes.py
│
├── forms.py
│
└── templates/tasks/
└── static/
Exercice — Créer un Blueprint — Déplacez les routes /tasks dans un blueprint tasks. Vérifiez ensuite flask --app ...
routes pour observer les nouveaux endpoints.
Page 26
Guide complet Flask — Édition 2026
18. Application Factory et extensions
18.1 Le pattern create_app()
Une application globale suffit pour un exercice simple, mais une factory permet de créer plusieurs instances de l’application
avec des configurations différentes. C’est particulièrement utile pour les tests et pour isoler proprement l’initialisation.
app/__init__.py
from flask import Flask
from .extensions import db, migrate, login_manager
def create_app(config_object=None):
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY="dev",
SQLALCHEMY_DATABASE_URI="sqlite:///taskflow.db",
)
if config_object:
app.config.from_object(config_object)
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
csrf.init_app(app)
from .main.routes import bp as main_bp
from .auth.routes import bp as auth_bp
from .tasks.routes import bp as tasks_bp
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp)
app.register_blueprint(tasks_bp)
return app
18.2 Extension non liée à une app globale
Pattern d’initialisation différée
# extensions.py
db = SQLAlchemy(model_class=Base)
login_manager = LoginManager()
migrate = Migrate()
# puis, dans create_app
db.init_app(app)
login_manager.init_app(app)
migrate.init_app(app, db)
18.3 Éviter les imports circulaires
Les imports circulaires apparaissent souvent lorsque models.py importe app alors que __init__.py importe models.py.
L’initialisation différée des extensions et les imports locaux des blueprints dans create_app() réduisent fortement ce risque.
Exercice — Factory testable — Transformez une petite application globale en create_app(config=None). Créez deux
instances avec des SECRET_KEY différentes et vérifiez qu’elles sont indépendantes.
Page 27
Guide complet Flask — Édition 2026
19. Construire une API JSON avec Flask
19.1 Une ressource, des verbes HTTP
Action
Méthode
Route
Statut typique
Lister
GET
/api/tasks
200
Lire
GET
/api/tasks/42
200/404
Créer
POST
/api/tasks
201/400
Modifier partiellement
PATCH
/api/tasks/42
200/404/422
Supprimer
DELETE
/api/tasks/42
204/404
19.2 Sérialiser explicitement
JSON
def task_to_dict(task):
return {
"id": task.id,
"title": task.title,
"done": task.done,
"created_at": task.created_at.isoformat(),
}
@bp.get("/tasks/<int:task_id>")
def get_task(task_id):
task = db.get_or_404(Task, task_id)
return task_to_dict(task)
19.3 Validation des payloads
POST JSON
@bp.post("/tasks")
def create_task():
data = request.get_json(silent=True) or {}
title = str(data.get("title", "")).strip()
if not title:
return {"error": "title is required"}, 400
task = Task(title=title, user_id=current_user.id)
db.session.add(task)
db.session.commit()
return task_to_dict(task), 201
19.4 Erreurs JSON cohérentes
Selon le type de client
@app.errorhandler(404)
def not_found(error):
if request.path.startswith("/api/"):
return {"error": "not_found"}, 404
return render_template("errors/404.html"), 404
19.5 Authentifier une API
Les cookies de session peuvent convenir à une API consommée par votre propre frontend, mais une API publique ou intersystème nécessite souvent une stratégie dédiée : jetons, OAuth2/OIDC ou clés de service, selon le risque. Flask ne vous
impose pas de solution unique.
Page 28
Guide complet Flask — Édition 2026
Exercice — Mini API — Exposez GET et POST /api/categories. Le POST doit valider name, créer la catégorie et
renvoyer 201.
Page 29
Guide complet Flask — Édition 2026
20. Uploads, fichiers et contenu utilisateur
20.1 Réception d’un fichier
Upload minimal
from pathlib import Path
from werkzeug.utils import secure_filename
UPLOAD_DIR = Path("uploads")
ALLOWED = {"png", "jpg", "jpeg", "pdf"}
def allowed_file(name):
return "." in name and name.rsplit(".", 1)[1].lower() in ALLOWED
@app.post("/upload")
def upload():
file = request.files.get("file")
if not file or not file.filename:
abort(400)
if not allowed_file(file.filename):
abort(415)
filename = secure_filename(file.filename)
file.save(UPLOAD_DIR / filename)
return {"filename": filename}, 201
20.2 Risques principaux





Nom de fichier traversant des répertoires : neutraliser avec secure_filename et, mieux encore, générer un nom serveur.
Extension trompeuse : ne considérez pas l’extension comme preuve du type réel du contenu.
Taille excessive : configurez MAX_CONTENT_LENGTH et des limites au reverse proxy.
Écrasement : utilisez des identifiants uniques, pas le nom original comme clé de stockage.
Publication directe : les fichiers utilisateurs nécessitent parfois une analyse antivirus, une politique de type MIME et un
stockage séparé.
Limiter la requête
app.config["MAX_CONTENT_LENGTH"] = 10 * 1024 * 1024
# 10 MiB
Exercice — Upload d’avatar — Acceptez uniquement png/jpeg, limite 2 MiB, générez un nom aléatoire côté serveur
et stockez le nom original uniquement comme métadonnée.
Page 30
Guide complet Flask — Édition 2026
21. Erreurs, journalisation et observabilité
21.1 abort() et handlers
Erreurs HTTP
from flask import abort
if not task:
abort(404)
@app.errorhandler(404)
def error_404(error):
return render_template("errors/404.html"), 404
21.2 Logging
Journaliser utilement
current_app.logger.info("Task %s created by user %s", task.id, current_user.id)
current_app.logger.warning("Invalid login attempt for %s", email)
current_app.logger.exception("Unexpected processing failure")
Un bon log aide à reconstruire ce qui s’est produit sans exposer des secrets. Ne journalisez pas les mots de passe, jetons
d’accès, clés d’API ou contenus hautement sensibles. Ajoutez un identifiant de corrélation lorsque plusieurs services
participent à une même transaction.
21.3 500 et exception non gérée
En développement, le débogueur montre la pile. En production, l’utilisateur doit recevoir une page générique tandis que
l’exception complète est envoyée aux logs et idéalement à une solution de suivi d’erreurs. La trace serveur est un artefact
technique, pas un contenu à exposer au navigateur.
21.4 Health checks
Liveness simple
@app.get("/health")
def health():
return {"status": "ok"}
Pour un système réel, distinguez souvent liveness et readiness. Une application peut être vivante mais incapable de servir
correctement si sa base de données ou un service critique est indisponible.
Exercice — Gestion des erreurs — Ajoutez des handlers 403, 404 et 500 avec templates dédiés. Vérifiez que les
codes HTTP renvoyés restent corrects, et pas seulement le texte de la page.
Page 31
Guide complet Flask — Édition 2026
22. Tester une application Flask avec pytest
22.1 Installer pytest
Dépendances de test
pip install pytest coverage
22.2 Fixture app et client
tests/conftest.py
import pytest
from app import create_app
from app.extensions import db
@pytest.fixture()
def app():
app = create_app({
"TESTING": True,
"SECRET_KEY": "test",
"SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:",
"WTF_CSRF_ENABLED": False,
})
with app.app_context():
db.create_all()
yield app
db.session.remove()
db.drop_all()
@pytest.fixture()
def client(app):
return app.test_client()
22.3 Test de route
Test simple
def test_home(client):
response = client.get("/")
assert response.status_code == 200
assert b"TaskFlow" in response.data
22.4 Tester POST et redirections
POST
def test_create_task(client):
response = client.post(
"/tasks/new",
data={"title": "Tester Flask"},
follow_redirects=True,
)
assert response.status_code == 200
assert b"Tester Flask" in response.data
22.5 Que tester ?




Vos règles : validations, permissions, transitions d’état, erreurs attendues.
Les routes : codes HTTP, redirections, contenu essentiel, authentification.
La base : contraintes et comportements de persistance importants.
Les cas négatifs : identifiants inconnus, données invalides, utilisateur non autorisé.
Page 32
Guide complet Flask — Édition 2026

Pas les bibliothèques : inutile de prouver que Flask lui-même sait router une URL.
Exécuter et mesurer
pytest -q
coverage run -m pytest
coverage report -m
Exercice — Tester une permission — Créez deux utilisateurs. Vérifiez que l’utilisateur B ne peut ni éditer ni
supprimer une tâche appartenant à A.
Page 33
Guide complet Flask — Édition 2026
23. Sécuriser une application Flask
23.1 Menaces à connaître
Menace
Erreur typique
Défense
XSS
Afficher du HTML utilisateur comme sûr
Auto-escaping, validation, CSP
CSRF
POST avec cookie de session sans jeton
Jeton CSRF / SameSite
Injection SQL
Concaténer du SQL avec entrée utilisateur
ORM / paramètres liés
IDOR
Charger /task/42 sans vérifier le propriétaire
Contrôle d’autorisation objet
Session theft
Cookie transmis sans HTTPS
Secure, HttpOnly, HTTPS
Brute force
Connexion sans limitation
Rate limiting, MFA selon risque
Upload malveillant
Faire confiance au nom/extension
Limites, renommage, validation, isolation
23.2 Cookies de session
Production sous HTTPS
app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax",
)
23.3 En-têtes de sécurité
Exemple de base à adapter
@app.after_request
def security_headers(response):
response.headers.setdefault("X-Content-Type-Options", "nosniff")
response.headers.setdefault("X-Frame-Options", "SAMEORIGIN")
response.headers.setdefault("Content-Security-Policy", "default-src 'self'")
return response
HSTS doit être activé uniquement lorsque le domaine est réellement servi en HTTPS de façon durable. Une CSP utile doit
être conçue selon les scripts, styles, images et intégrations du site ; recopier une politique stricte sans comprendre les
ressources nécessaires peut casser l’application.
23.4 SECRET_KEY
La clé secrète doit être longue, aléatoire, différente selon les environnements et gérée hors du dépôt Git. La changer
invalide les sessions et données signées dépendantes ; planifiez la rotation lorsque l’application a des utilisateurs actifs.
23.5 Validation et principe du moindre privilège
Validez longueur, type, format et domaine métier des entrées. Côté base, utilisez un compte ayant uniquement les privilèges
nécessaires. Côté application, ne donnez pas de droits administratifs pour contourner une difficulté fonctionnelle.
23.6 Sécurité du serveur de développement
Interdit en production — Le serveur de développement et le débogueur interactif Flask ne sont pas conçus pour la
production. Le débogueur peut devenir une voie d’exécution de code si l’environnement est exposé.
Page 34
Guide complet Flask — Édition 2026
Exercice — Checklist sécurité — Passez votre application en revue : secret, cookies, CSRF, XSS, contrôles d’objet,
uploads, headers, debug, dépendances. Notez les écarts avant de corriger.
Page 35
Guide complet Flask — Édition 2026
24. Performance, tâches longues et async
24.1 Mesurer avant d’optimiser
Une application lente peut être bloquée par la base de données, un appel réseau, un template, un fichier, un manque d’index
ou une architecture de workers inadéquate. Mesurez le temps des requêtes et inspectez les requêtes SQL avant d’ajouter du
cache partout.
24.2 Index et requêtes
Les colonnes souvent filtrées ou utilisées dans les jointures peuvent nécessiter des index. Un index accélère certaines
lectures mais augmente le coût des écritures et occupe de l’espace. Il doit refléter les vrais patterns de requêtes.
24.3 Cache
Le cache peut viser une fonction, une requête coûteuse, une page ou une réponse API. Toute stratégie de cache doit
répondre à une question difficile : quand la valeur devient-elle invalide ? Un cache rapide mais incohérent est une anomalie
fonctionnelle, pas une optimisation.
24.4 Tâches longues
Une requête Web ne devrait pas attendre plusieurs minutes pour un traitement lourd. Utilisez une file de tâches telle que
Celery/RQ ou une infrastructure équivalente, puis exposez l’état du job à l’utilisateur. Flask ne doit pas lancer naïvement
une tâche de fond avec asyncio.create_task depuis une vue : l’event loop de la vue s’arrête à la fin de la requête.
24.5 async def dans Flask
Support async
pip install "flask[async]"
Vue async
@app.get("/aggregate")
async def aggregate():
a = await call_service_a()
b = await call_service_b()
return {"a": a, "b": b}
Flask reste WSGI : une vue async ne transforme pas automatiquement l’application en serveur massivement concurrent.
Elle est utile surtout pour composer des opérations I/O asynchrones dans une requête. Pour une application majoritairement
async ou WebSocket, considérez un framework ASGI-first.
Exercice — Trouver le goulot — Ajoutez des mesures de durée autour d’une requête SQL et d’un appel externe
simulé. Identifiez lequel domine la latence avant de proposer une optimisation.
Page 36
Guide complet Flask — Édition 2026
25. Déployer Flask en production
25.1 Le serveur de développement n’est pas un serveur de production
En production, Flask est servi par un serveur WSGI dédié tel que Gunicorn ou Waitress, souvent derrière un reverse proxy
tel que Nginx, Apache ou un service managé. Le reverse proxy peut gérer TLS, compression, fichiers statiques, limites et
en-têtes réseau.
Architecture classique
Navigateur
| HTTPS
v
Reverse proxy / Load balancer
| HTTP local / réseau privé
v
Serveur WSGI (Gunicorn / Waitress / autre)
|
v
Application Flask
|
+--> Base de données / cache / services
25.2 Gunicorn (Linux)
Exemple à adapter
pip install gunicorn
gunicorn "app:create_app()" --workers 4 --bind 127.0.0.1:8000
25.3 Waitress (Windows ou multiplateforme)
Exemple
pip install waitress
waitress-serve --call app:create_app
25.4 Variables de production







SECRET_KEY forte et injectée par l’environnement / secret manager.
DATABASE_URL pointant vers le serveur de base de données réel.
DEBUG désactivé.
Cookies Secure sous HTTPS.
Logs collectés hors du seul fichier local éphémère.
Migrations appliquées de façon contrôlée.
Sauvegardes et restauration testées.
25.5 ProxyFix
Lorsqu’un reverse proxy transmet l’adresse client, le protocole HTTPS ou le host via des en-têtes X-Forwarded-*,
Flask/Werkzeug peut utiliser ProxyFix. Il faut le configurer avec le nombre exact de proxies de confiance ; une mauvaise
configuration peut faire confiance à des en-têtes falsifiés.
25.6 Plateformes managées
La documentation Flask mentionne notamment PythonAnywhere, Google Cloud Run, AWS Elastic Beanstalk et Microsoft
Azure. Une plateforme managée réduit la charge d’exploitation, mais ne supprime pas les responsabilités de configuration,
secrets, migrations, logs, sécurité et sauvegardes.
Page 37
Guide complet Flask — Édition 2026
Exercice — Préparer le déploiement — Écrivez une checklist de passage en production de votre projet :
dépendances, variables, base, migrations, WSGI, HTTPS, logs, sauvegarde, health check et rollback.
Page 38
Guide complet Flask — Édition 2026
26. Projet fil rouge : TaskFlow
TaskFlow est une petite application de gestion de tâches multi-utilisateur. Elle est suffisamment simple pour être comprise
entièrement, mais assez riche pour mobiliser les patterns d’un projet réel : factory, Blueprints, modèles, formulaires,
authentification, permissions, migrations, tests et configuration.
26.1 Fonctionnalités cibles








Inscription et connexion utilisateur.
Création, modification, clôture et suppression de tâches.
Catégories optionnelles et priorité.
Chaque utilisateur ne voit et ne modifie que ses propres tâches.
Page HTML et mini API JSON en lecture.
Validation CSRF des formulaires.
Migrations de base de données.
Tests des routes critiques et des permissions.
26.2 Arborescence finale
Structure
taskflow/
├── pyproject.toml
├── migrations/
├── app/
│
├── __init__.py
│
├── extensions.py
│
├── models.py
│
├── main/
│
│
├── __init__.py
│
│
└── routes.py
│
├── auth/
│
│
├── __init__.py
│
│
├── forms.py
│
│
├── routes.py
│
│
└── templates/auth/
│
├── tasks/
│
│
├── __init__.py
│
│
├── forms.py
│
│
├── routes.py
│
│
└── templates/tasks/
│
├── api/
│
│
├── __init__.py
│
│
└── routes.py
│
├── templates/
│
│
├── base.html
│
│
└── errors/
│
└── static/css/app.css
└── tests/
├── conftest.py
├── test_auth.py
└── test_tasks.py
Page 39
Guide complet Flask — Édition 2026
26.3 extensions.py
app/extensions.py
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
db = SQLAlchemy(model_class=Base)
migrate = Migrate()
login_manager = LoginManager()
csrf = CSRFProtect()
login_manager.login_view = "auth.login"
login_manager.login_message = "Veuillez vous connecter."
26.4 Modèles
app/models.py
from datetime import datetime, timezone
from flask_login import UserMixin
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from werkzeug.security import check_password_hash, generate_password_hash
from .extensions import db
class User(UserMixin, db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
password_hash: Mapped[str] = mapped_column(String(255))
tasks: Mapped[list["Task"]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)
def set_password(self, password: str) -> None:
self.password_hash = generate_password_hash(password)
def check_password(self, password: str) -> bool:
return check_password_hash(self.password_hash, password)
class Task(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(120))
description: Mapped[str | None] = mapped_column(Text, nullable=True)
done: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), index=True)
user: Mapped["User"] = relationship(back_populates="tasks")
Page 40
Guide complet Flask — Édition 2026
26.5 Factory
app/__init__.py
import os
from flask import Flask
from .extensions import csrf, db, login_manager, migrate
def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY=os.environ.get("SECRET_KEY", "dev-only-change-me"),
SQLALCHEMY_DATABASE_URI=os.environ.get(
"DATABASE_URL", "sqlite:///taskflow.db"
),
SQLALCHEMY_TRACK_MODIFICATIONS=False,
)
if test_config:
app.config.update(test_config)
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
csrf.init_app(app)
from .models import User
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))
from .main.routes import bp as main_bp
from .auth.routes import bp as auth_bp
from .tasks.routes import bp as tasks_bp
from .api.routes import bp as api_bp
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp)
app.register_blueprint(tasks_bp)
app.register_blueprint(api_bp)
return app
26.6 Formulaire de tâche
app/tasks/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length
class TaskForm(FlaskForm):
title = StringField("Titre", validators=[DataRequired(), Length(max=120)])
description = TextAreaField("Description", validators=[Length(max=2000)])
submit = SubmitField("Enregistrer")
Page 41
Guide complet Flask — Édition 2026
26.7 Routes des tâches
app/tasks/routes.py
Page 42
Guide complet Flask — Édition 2026
from flask import Blueprint, abort, flash, redirect, render_template, url_for
from flask_login import current_user, login_required
from .forms import TaskForm
from ..extensions import db
from ..models import Task
bp = Blueprint("tasks", __name__, url_prefix="/tasks")
def owned_task_or_404(task_id: int) -> Task:
task = db.get_or_404(Task, task_id)
if task.user_id != current_user.id:
abort(403)
return task
@bp.get("/")
@login_required
def index():
stmt = (
db.select(Task)
.where(Task.user_id == current_user.id)
.order_by(Task.created_at.desc())
)
tasks = db.session.execute(stmt).scalars().all()
return render_template("tasks/index.html", tasks=tasks)
@bp.route("/new", methods=["GET", "POST"])
@login_required
def create():
form = TaskForm()
if form.validate_on_submit():
task = Task(
title=form.title.data.strip(),
description=form.description.data.strip() or None,
user_id=current_user.id,
)
db.session.add(task)
db.session.commit()
flash("Tâche créée.", "success")
return redirect(url_for("tasks.index"))
return render_template("tasks/form.html", form=form, page_title="Nouvelle tâche")
@bp.route("/<int:task_id>/edit", methods=["GET", "POST"])
@login_required
def edit(task_id):
task = owned_task_or_404(task_id)
form = TaskForm(obj=task)
if form.validate_on_submit():
task.title = form.title.data.strip()
task.description = form.description.data.strip() or None
db.session.commit()
flash("Tâche modifiée.", "success")
return redirect(url_for("tasks.index"))
return render_template("tasks/form.html", form=form, page_title="Modifier")
@bp.post("/<int:task_id>/toggle")
@login_required
def toggle(task_id):
task = owned_task_or_404(task_id)
task.done = not task.done
db.session.commit()
return redirect(url_for("tasks.index"))
@bp.post("/<int:task_id>/delete")
@login_required
def delete(task_id):
task = owned_task_or_404(task_id)
db.session.delete(task)
db.session.commit()
flash("Tâche supprimée.", "success")
return redirect(url_for("tasks.index"))
Page 43
Guide complet Flask — Édition 2026
26.8 Authentification : logique essentielle
Extrait de auth/routes.py
stmt = db.select(User).where(User.email == form.email.data.lower().strip())
user = db.session.execute(stmt).scalar_one_or_none()
if user is None or not user.check_password(form.password.data):
flash("Identifiants invalides.", "error")
else:
login_user(user, remember=form.remember.data)
return redirect(url_for("tasks.index"))
26.9 Template de liste
app/tasks/templates/tasks/index.html
{% extends "base.html" %}
{% block title %}Mes tâches{% endblock %}
{% block content %}
<h1>Mes tâches</h1>
<a href="{{ url_for('tasks.create') }}">Nouvelle tâche</a>
{% for task in tasks %}
<article>
<h2>{{ task.title }}</h2>
{% if task.description %}<p>{{ task.description }}</p>{% endif %}
<form method="post" action="{{ url_for('tasks.toggle', task_id=task.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button>{{ "Rouvrir" if task.done else "Terminer" }}</button>
</form>
<a href="{{ url_for('tasks.edit', task_id=task.id) }}">Modifier</a>
</article>
{% else %}
<p>Aucune tâche.</p>
{% endfor %}
{% endblock %}
Formulaires d’action — Les formulaires FlaskForm utilisent hidden_tag(). Pour un petit formulaire HTML manuel
comme toggle/delete, CSRFProtect permet d’envoyer le jeton dans un champ caché nommé csrf_token. Pour une base
de code homogène, vous pouvez aussi créer un FlaskForm vide dédié aux actions POST.
26.10 Étapes de réalisation
6. Créer l’environnement et les dépendances.
7. Créer extensions.py, models.py et create_app().
8. Créer les Blueprints main, auth, tasks et api.
9. Initialiser Flask-Migrate et appliquer la première migration.
10. Construire l’inscription et la connexion.
11. Construire le CRUD des tâches avec contrôle d’appartenance.
12. Ajouter base.html, les messages flash et un CSS minimal.
13. Écrire les tests de connexion, création et autorisation.
14. Configurer secrets et cookies pour la production.
15. Déployer avec un serveur WSGI dédié.
26.11 Critères de qualité du projet
Critère
Question de revue
Structure
Les domaines sont-ils séparés en Blueprints cohérents ?
Sécurité
Tous les POST sensibles ont-ils CSRF et autorisation ?
Page 44
Guide complet Flask — Édition 2026
Données
Les contraintes existent-elles dans la base, pas seulement dans les
formulaires ?
Transactions
Chaque modification est-elle commitée au bon moment ?
URLs
Les liens utilisent-ils url_for() ?
Tests
Les cas d’échec et les permissions sont-ils testés ?
Config
Les secrets sont-ils hors du code ?
Production
Debug est-il désactivé et un WSGI server est-il utilisé ?
Page 45
Guide complet Flask — Édition 2026
27. Débogage : erreurs fréquentes et méthode de
diagnostic
27.1 “Working outside of application context”
Cause : accès à current_app, db.session ou une extension contextuelle hors contexte d’application. Correction : déplacer le
code dans une requête/commande Flask ou utiliser with app.app_context(): lorsque l’opération hors requête est légitime.
27.2 BuildError avec url_for()
Cause : mauvais endpoint, paramètres manquants ou oubli du préfixe de Blueprint. Inspectez flask --app ... routes et utilisez
par exemple url_for('tasks.edit', task_id=task.id).
27.3 TemplateNotFound
Vérifiez le nom exact, le dossier templates, l’emplacement relatif du blueprint et la casse. Un template
templates/tasks/index.html se charge généralement avec render_template('tasks/index.html').
27.4 405 Method Not Allowed
La route existe mais n’accepte pas la méthode envoyée. Comparez le method du formulaire, les décorateurs
@bp.get/@bp.post et la requête réellement envoyée.
27.5 CSRF token missing / invalid
Vérifiez SECRET_KEY, le champ hidden_tag()/csrf_token, la présence d’un vrai POST, la session/cookie et le fait que le
formulaire ait été généré dans le même environnement que celui qui le valide.
27.6 IntegrityError
Souvent une contrainte UNIQUE, NOT NULL ou FOREIGN KEY est violée. Lisez le message de la base. Après une
exception SQLAlchemy pendant la transaction, effectuez db.session.rollback() si vous continuez le traitement dans le même
contexte.
27.7 ImportError / circular import
Symptôme fréquent des projets structurés trop tôt autour d’une instance app globale. Utilisez une factory, initialisez les
extensions sans app puis init_app(), et évitez que models.py importe l’application.
27.8 Méthode de diagnostic
16. Lire la dernière exception réelle de la stack trace, pas seulement la première ligne visible.
17. Identifier la route et la méthode HTTP concernées.
18. Reproduire avec la plus petite entrée possible.
19. Vérifier les logs côté serveur et l’onglet Network du navigateur.
20. Inspecter les routes avec flask routes.
21. Inspecter le SQL ou la base lorsqu’une persistance est impliquée.
22. Écrire un test reproduisant le bug avant ou en même temps que la correction.
Exercice — Diagnostic — Provoquez volontairement un BuildError, un 405 et une IntegrityError dans un projet de
test. Documentez pour chacun le symptôme, la cause et la correction.
Page 46
Guide complet Flask — Édition 2026
28. Bonnes pratiques d’architecture et revue de code
28.1 Garder les vues minces
Une vue doit recevoir la requête, déclencher les validations et autorisations, appeler la logique métier/persistance puis
construire la réponse. Si une fonction de vue fait 150 lignes et contient plusieurs branches métier, extrayez des fonctions ou
services testables.
28.2 Nommage et endpoints
Utilisez des noms stables et explicites : auth.login, tasks.create, tasks.edit. L’URL peut changer sans obliger tout le code à
changer si url_for() est utilisé partout.
28.3 Contraintes à plusieurs niveaux
Une validation importante peut exister à trois niveaux : interface (confort), serveur (message métier) et base de données
(intégrité). Ces couches sont complémentaires, pas redondantes.
28.4 Transactions et idempotence
Définissez clairement ce qui constitue une unité atomique. Évitez de commit au milieu d’une opération métier puis de
poursuivre avec une seconde étape qui peut échouer, sauf si ce découpage est voulu. Pour les intégrations externes, étudiez
l’idempotence et les reprises.
28.5 Configuration et dépendances
Épinglez raisonnablement les versions, automatisez les tests et séparez les dépendances de développement. L’objectif n’est
pas de ne jamais mettre à jour, mais de pouvoir expliquer et reproduire ce qui s’exécute.
28.6 Checklist de revue










La route correspond-elle à la bonne méthode HTTP ?
Les entrées sont-elles validées côté serveur ?
Le contrôle d’autorisation porte-t-il sur la ressource précise ?
Le template échappe-t-il le contenu utilisateur ?
Le POST est-il protégé contre CSRF ?
La requête SQL est-elle ciblée et indexable ?
Le comportement d’erreur est-il explicite ?
Le code est-il testable sans lancer un vrai serveur ?
Un secret ou une donnée sensible apparaît-il dans le code ou les logs ?
La fonction de vue orchestre-t-elle plutôt qu’elle n’absorbe toute la logique ?
28.7 Évolution vers des architectures plus complexes
Lorsque l’application grandit, vous pouvez introduire des couches de service, repositories, DTO/schémas, injection de
dépendances légère, tâches asynchrones et modules métiers. N’ajoutez toutefois pas ces abstractions avant d’avoir un
problème réel à résoudre. Flask récompense les architectures explicites, mais ne nécessite pas de reproduire un framework
d’entreprise à l’intérieur du framework.
Page 47
Guide complet Flask — Édition 2026
29. Exercices de synthèse et pistes de correction
29.1 Niveau 1 — fondamentaux
23. Créer une application avec /, /about et /hello/<name>.
24. Créer /convert?celsius=... qui renvoie Fahrenheit en JSON.
25. Créer un template base.html et deux pages qui en héritent.
26. Créer un formulaire de contact validant nom, email et message.
29.2 Niveau 2 — données
27. Créer les modèles Author et Book en relation one-to-many.
28. Écrire les routes de création et de liste de livres.
29. Empêcher deux auteurs d’avoir le même email avec une contrainte UNIQUE.
30. Ajouter une migration créant une colonne published_at.
29.3 Niveau 3 — sécurité et utilisateurs
31. Ajouter User + authentification Flask-Login.
32. Restreindre /books/new aux utilisateurs connectés.
33. Ajouter owner_id à Book et interdire l’édition par un autre utilisateur.
34. Ajouter des tests prouvant que le contrôle fonctionne.
29.4 Niveau 4 — API et production
35. Exposer GET /api/books et GET /api/books/<id>.
36. Ajouter un format d’erreur JSON stable.
37. Ajouter /health.
38. Préparer une configuration de production et une commande WSGI.
29.5 Pistes de correction
Les solutions ne doivent pas être comparées caractère par caractère. Évaluez plutôt les invariants : bonnes méthodes HTTP,
validation serveur, requêtes SQLAlchemy 2.x, contrôle d’autorisation par ressource, redirection après POST HTML, codes
d’erreur corrects, configuration externe des secrets et tests isolés.
Critère de maîtrise — Vous maîtrisez les fondamentaux de Flask lorsque vous pouvez expliquer où se trouve chaque
responsabilité, diagnostiquer une erreur de contexte ou de routage, et construire une fonctionnalité CRUD avec
authentification, autorisation, migration et tests sans tout placer dans app.py.
Page 48
Guide complet Flask — Édition 2026
Annexe A — Commandes Flask et Python utiles
Commande
Usage
python -m venv .venv
Créer un environnement virtuel
.venv\Scripts\activate
Activer sous Windows
source .venv/bin/activate
Activer sous macOS/Linux
pip install Flask
Installer Flask
flask --app app run --debug
Serveur de développement
flask --app app routes
Lister les routes
flask --app app shell
Shell avec contexte Flask
flask db migrate -m "..."
Générer une migration
flask db upgrade
Appliquer les migrations
pytest -q
Exécuter les tests
coverage run -m pytest
Tests avec couverture
Page 49
Guide complet Flask — Édition 2026
Annexe B — Objets Flask à connaître
Objet / fonction
Rôle
Flask
Classe principale de l’application
Blueprint
Groupe de routes/composant
request
Requête HTTP courante
session
Session signée de l’utilisateur
g
Stockage contextuel temporaire
current_app
Application courante
render_template
Rendre un template Jinja
url_for
Générer une URL par endpoint
redirect
Créer une redirection
flash
Message temporaire
abort
Déclencher une erreur HTTP
jsonify / dict return
Construire une réponse JSON
make_response
Construire/ajuster une Response
Page 50
Guide complet Flask — Édition 2026
Annexe C — Fiche mémo SQLAlchemy 2.x
Référence rapide
# SELECT plusieurs
stmt = db.select(Task).where(Task.done.is_(False))
tasks = db.session.execute(stmt).scalars().all()
# SELECT un
task = db.session.execute(stmt).scalar_one_or_none()
# GET par PK
task = db.session.get(Task, task_id)
# Flask helper 404
task = db.get_or_404(Task, task_id)
# INSERT
task = Task(title="Exemple")
db.session.add(task)
db.session.commit()
# UPDATE
task.title = "Nouveau titre"
db.session.commit()
# DELETE
db.session.delete(task)
db.session.commit()
# ROLLBACK
db.session.rollback()
Page 51
Guide complet Flask — Édition 2026
Annexe D — Codes HTTP essentiels
Code
Nom simplifié
Quand l’utiliser
200
OK
Succès standard
201
Created
Ressource créée
204
No Content
Succès sans corps
301/308
Redirect permanent
URL déplacée durablement
302/303
Redirect
Navigation après une action
400
Bad Request
Entrée syntaxiquement/intrinsèquement
invalide
401
Unauthorized
Authentification requise
403
Forbidden
Droit insuffisant
404
Not Found
Ressource inconnue
405
Method Not Allowed
Mauvaise méthode HTTP
409
Conflict
Conflit avec l’état actuel
413
Content Too Large
Upload/requête trop volumineux
415
Unsupported Media Type
Type de contenu non accepté
422
Unprocessable Content
Validation métier échouée
429
Too Many Requests
Limitation de débit
500
Internal Server Error
Erreur inattendue côté serveur
503
Service Unavailable
Service temporairement indisponible
Page 52
Guide complet Flask — Édition 2026
Annexe E — Glossaire
Terme
Définition
Application Factory
Fonction, souvent create_app(), qui construit et configure une
instance Flask.
Blueprint
Composant enregistrable regroupant routes et ressources.
Context
Mécanisme qui associe temporairement request/current_app/g à
l’exécution courante.
CSRF
Attaque utilisant le navigateur d’un utilisateur authentifié pour
envoyer une action non voulue.
Endpoint
Nom logique d’une vue utilisé notamment par url_for().
Jinja
Moteur de templates utilisé par Flask.
Migration
Script versionné faisant évoluer le schéma de base de données.
ORM
Object-Relational Mapper : manipulation de données relationnelles
via objets et expressions Python.
Reverse proxy
Serveur placé devant l’application, souvent pour TLS, routage,
sécurité, compression et fichiers statiques.
Route
Règle associant une URL/méthode HTTP à une fonction de vue.
Session
Mécanisme permettant de conserver un état entre requêtes d’un
client.
WSGI
Interface standard Python entre application Web et serveur WSGI.
XSS
Injection de contenu actif dans une page Web affichée à un autre
utilisateur.
IDOR
Accès non autorisé à un objet en manipulant un identifiant direct.
PRG
Post/Redirect/Get : redirection après un POST réussi.
Page 53
Guide complet Flask — Édition 2026
Annexe F — Références officielles et notes de version
Les concepts et recommandations de ce manuel ont été alignés sur les documentations officielles suivantes, consultées pour
l’édition de septembre 2026. Les bibliothèques évoluent : pour une nouvelle application, vérifiez toujours les changelogs
avant de figer les versions.














Flask 3.1.x — documentation — https://flask.palletsprojects.com/en/stable/
Flask — installation — https://flask.palletsprojects.com/en/stable/installation/
Flask — quickstart — https://flask.palletsprojects.com/en/stable/quickstart/
Flask — application factories — https://flask.palletsprojects.com/en/stable/patterns/appfactories/
Flask — Blueprints — https://flask.palletsprojects.com/en/stable/blueprints/
Flask — testing — https://flask.palletsprojects.com/en/stable/testing/
Flask — security considerations — https://flask.palletsprojects.com/en/stable/web-security/
Flask — deploying to production — https://flask.palletsprojects.com/en/stable/deploying/
Flask — async/await — https://flask.palletsprojects.com/en/stable/async-await/
Flask-SQLAlchemy 3.1.x — https://flask-sqlalchemy.palletsprojects.com/en/stable/
Flask-Migrate — https://flask-migrate.readthedocs.io/en/latest/
Flask-Login 0.7 — https://flask-login.readthedocs.io/en/latest/
Flask-WTF 1.3.x — https://flask-wtf.readthedocs.io/en/latest/
Alembic — https://alembic.sqlalchemy.org/en/latest/
Notes importantes sur les versions





Flask 3.1.x est la branche stable de référence utilisée ici ; la documentation officielle indique le support de Python 3.9
et plus.
Flask-SQLAlchemy 3.1.x est conçu autour de SQLAlchemy 2.x ; l’interface Model.query est considérée comme
historique/legacy et le style select/session moderne est privilégié.
Flask-WTF 1.3.0, publié en avril 2026, ne prend plus en charge Python 3.9 ; ce guide recommande donc Python 3.12+
pour un nouvel environnement.
Le serveur de développement Flask et le mode debug ne doivent pas être utilisés en production.
Les vues async Flask ne rendent pas WSGI équivalent à un framework ASGI-first et ne doivent pas être utilisées pour
lancer naïvement des tâches de fond après la réponse.
Page 54
Guide complet Flask — Édition 2026
Annexe G — Parcours de maîtrise en 30 jours
Période
Objectif
Livrable
Jours 1–3
HTTP, routes, request/response
Mini site 3 pages + API status
Jours 4–6
Jinja, static, flash
Interface avec héritage de templates
Jours 7–10
Formulaires, CSRF, config
Formulaire validé et secrets externalisés
Jours 11–15
SQLAlchemy, CRUD, relations
Application CRUD avec migration
Jours 16–19
Auth, permissions
Multi-utilisateur avec contrôle d’objet
Jours 20–22
Blueprints + factory
Restructuration modulaire
Jours 23–25
API JSON + erreurs
Mini API documentée
Jours 26–28
Tests + sécurité
Suite pytest + checklist sécurité
Jours 29–30
Déploiement + revue
Application servie par WSGI + revue finale
Conclusion
Flask est volontairement simple au centre et exigeant aux bords. La compétence ne consiste pas à connaître beaucoup de
décorateurs, mais à savoir où placer le routage, la validation, la logique métier, l’accès aux données, les permissions et la
configuration ; à tester les comportements critiques ; puis à exécuter l’application dans un environnement de production
approprié. Si ces responsabilités restent visibles et explicites, Flask demeure lisible même lorsque le projet grandit.
Suite recommandée — Après ce manuel, construisez TaskFlow sans copier le chapitre 26. Utilisez-le seulement
comme référence lorsque vous bloquez. Cette reconstruction de mémoire est l’exercice qui transforme les notions en
savoir-faire.
Page 55
Téléchargement