23 - Flask
Ouverture. La bibliothèque Flask n’est pas au programme de NSI, mais ce qu’elle met en œuvre l’est : le dialogue entre un client et un serveur, les requêtes HTTP, les méthodes GET et POST et les formulaires, vus dans le cours IHM Web . Avec Flask, c’est nous qui écrivons le serveur, en Python. Cette page suppose connus les cours sur le HTML et le CSS . Les programmes s’exécutent sur l’ordinateur, dans Thonny ou un autre éditeur, pas dans le navigateur.
Le principe : un serveur écrit en Python
Quand on visite une page, le navigateur (le client) envoie une requête HTTP à un serveur, par exemple GET /bonjour. Le serveur renvoie une réponse qui contient du HTML, que le navigateur affiche.
Jusqu’ici, nos pages HTML étaient des fichiers écrits à l’avance : le serveur renvoyait toujours le même contenu. Avec Flask, à chaque adresse du site correspond une fonction Python. Quand une requête arrive, Flask appelle la fonction de cette adresse, et ce qu’elle renvoie devient la page. La page peut donc changer à chaque visite : résultat d’un calcul, réponse à un formulaire, score d’un joueur.
Une page fabriquée au moment de la requête s’appelle une page dynamique ; un fichier HTML renvoyé tel quel, une page statique.
Installer Flask et lancer un premier site
Flask s’installe une fois sur l’ordinateur :
- dans Thonny : menu Outils, puis Gérer les paquets, chercher
flasket cliquer sur Installer ; - dans un terminal :
pip install flask.
On crée un dossier pour le projet, et dans ce dossier un fichier app.py.
Fichier app.py :
from flask import Flask
app = Flask(__name__) # l'application, c'est-à-dire le site
@app.route("/") # adresse de la page d'accueil
def accueil():
return "<h1>Bonjour !</h1><p>Mon premier site avec Flask.</p>"
@app.route("/apropos") # adresse /apropos
def apropos():
return "<p>Site réalisé en première NSI.</p>"
if __name__ == "__main__":
app.run(debug=True) # démarrer le serveur
On exécute app.py comme n’importe quel programme. Le terminal affiche :
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
Le message WARNING rappelle que ce serveur sert à développer, pas à publier un site.
Le programme ne s’arrête pas : c’est un serveur, qui attend les requêtes. On ouvre alors un navigateur à l’adresse http://127.0.0.1:5000.

La page d’accueil, servie par notre propre serveur.
127.0.0.1est l’adresse IP de l’ordinateur lui-même (le nomlocalhostdésigne cette même adresse) : le client et le serveur sont ici sur la même machine.5000est le port sur lequel le serveur écoute.@app.route("/")est un décorateur : il associe l’adresse/à la fonction écrite juste en dessous. Une fonction ainsi associée à une adresse s’appelle une vue.- La vue renvoie une chaîne : c’est le HTML de la page.
- Avec
debug=True, le serveur redémarre tout seul quand on enregistreapp.py, et une erreur affiche une page d’explication dans le navigateur. Ce mode ne sert qu’au développement, sur son propre ordinateur. - Pour arrêter le serveur :
Ctrl+Cdans le terminal, ou le bouton Stop de Thonny.
Question. Que renvoie le serveur si l’on visite http://127.0.0.1:5000/contact ?
Réponse
Une page d’erreur « Not Found » avec le code HTTP 404 : aucune vue n’est associée à l’adresse /contact. Le terminal du serveur affiche la ligne "GET /contact HTTP/1.1" 404.
Le bloc if __name__ == "__main__":
La variable spéciale __name__ vaut "__main__" quand le fichier est exécuté, et le nom du module quand il est importé par un autre fichier. Le serveur ne démarre donc que si l’on lance app.py lui-même. C’est important dès qu’un projet a plusieurs fichiers : un fichier jeu.py importé par app.py ne doit contenir que des fonctions, et tout code de test doit être placé dans un tel bloc, sinon il s’exécute à chaque import.
Des adresses avec des paramètres
Une partie de l’adresse peut être variable : on l’écrit entre chevrons dans la route, et Flask la transmet à la vue comme paramètre.
Fichier app.py :
from flask import Flask
app = Flask(__name__)
@app.route("/bonjour/<prenom>")
def bonjour(prenom):
return f"<h1>Bonjour {prenom} !</h1>"
@app.route("/table/<int:n>") # int: convertit en entier
def table(n):
lignes = ""
for i in range(1, 11):
lignes = lignes + f"<li>{n} x {i} = {n * i}</li>"
return f"<h1>Table de {n}</h1><ul>{lignes}</ul>"
if __name__ == "__main__":
app.run(debug=True)

L’adresse /table/7 : une seule vue pour toutes les tables.
| Adresse visitée | Vue appelée | Paramètre reçu |
|---|---|---|
/bonjour/Lina | bonjour | prenom = "Lina" (une chaîne) |
/table/7 | table | n = 7 (un entier) |
/table/sept | aucune : erreur 404 | sept n’est pas un entier |
Question. Sans int:, que renverrait la vue table pour l’adresse /table/7 ?
Réponse
Le paramètre serait la chaîne "7". Le calcul n * i répéterait alors la chaîne : la page afficherait par exemple 7 x 3 = 777. Le convertisseur int: fait la conversion, comme int().
Les gabarits : séparer le HTML du Python
Écrire du HTML dans des chaînes Python devient vite illisible. Flask range le HTML dans des fichiers à part, les gabarits (templates), placés dans un dossier nommé exactement templates, à côté de app.py. La fonction render_template remplit un gabarit avec des valeurs calculées en Python.
notes/
├── app.py
└── templates/
└── notes.html
Fichier app.py :
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def notes():
eleve = "Lina"
resultats = {"Maths": 15, "NSI": 18, "Physique": 9, "Anglais": 13}
moyenne = sum(resultats.values()) / len(resultats)
return render_template("notes.html", eleve=eleve, resultats=resultats, moyenne=moyenne)
if __name__ == "__main__":
app.run(debug=True)
Fichier templates/notes.html :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Notes de {{ eleve }}</title>
</head>
<body>
<h1>Notes de {{ eleve }}</h1>
<ul>
{% for matiere, note in resultats.items() %}
{% if note >= 10 %}
<li>{{ matiere }} : {{ note }}</li>
{% else %}
<li style="color: crimson">{{ matiere }} : {{ note }} (à retravailler)</li>
{% endif %}
{% endfor %}
</ul>
<p>Moyenne : {{ moyenne | round(1) }}</p>
</body>
</html>

Le gabarit rempli : la note sous la moyenne est mise en évidence.
Le gabarit est du HTML, enrichi d’un petit langage appelé Jinja :
| Écriture | Rôle | Exemple |
|---|---|---|
{{ expression }} | insérer une valeur | {{ eleve }}, {{ note * 2 }} |
{% for x in liste %} … {% endfor %} | répéter un morceau de HTML | une ligne <li> par matière |
{% if condition %} … {% else %} … {% endif %} | choisir un morceau de HTML | couleur selon la note |
{{ x | filtre }} | transformer une valeur | round(1), upper, length |
Chaque variable utilisée dans le gabarit doit être transmise par render_template, sous la forme nom_dans_le_gabarit=valeur.
Question. Que contient la page si l’on écrit render_template("notes.html", eleve=eleve) ?
Réponse
Une erreur jinja2.exceptions.UndefinedError: 'resultats' is undefined : la variable resultats n’a pas été transmise, et le gabarit appelle sa méthode items(). Une variable absente simplement affichée, comme {{ eleve }} si on ne l’avait pas transmise, ne provoque pas d’erreur : elle donne un texte vide, ce qui rend l’oubli plus difficile à repérer. Passée à un filtre, comme {{ moyenne | round(1) }}, elle provoque en revanche une erreur.
Jinja protège automatiquement le texte inséré : si une variable contient <script>, il est affiché tel quel et non exécuté. Un utilisateur ne peut donc pas injecter de code dans la page en tapant du HTML dans un formulaire.
Une mise en page commune et des fichiers statiques
Toutes les pages d’un site partagent le même en-tête, le même menu, la même feuille de style. Plutôt que de les recopier dans chaque gabarit, on écrit un gabarit de base qui contient des blocs ; chaque page hérite de la base et ne remplit que ses blocs.
Les fichiers qui ne changent jamais (feuilles de style CSS, images, scripts JavaScript) sont les fichiers statiques : on les range dans un dossier nommé exactement static.
mon_site/
├── app.py
├── static/
│ └── style.css
└── templates/
├── base.html
├── accueil.html
└── jeux.html
Fichier templates/base.html :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>{% block titre %}Mon site{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav>
<a href="{{ url_for('accueil') }}">Accueil</a>
<a href="{{ url_for('jeux') }}">Jeux</a>
</nav>
<main>
{% block contenu %}{% endblock %}
</main>
</body>
</html>
Fichier templates/accueil.html :
{% extends "base.html" %}
{% block titre %}Accueil{% endblock %}
{% block contenu %}
<h1>Bienvenue</h1>
<p>Ce site rassemble nos projets de première NSI.</p>
{% endblock %}
Fichier templates/jeux.html :
{% extends "base.html" %}
{% block titre %}Jeux{% endblock %}
{% block contenu %}
<h1>Nos jeux</h1>
<ul>
{% for jeu in jeux %}
<li>{{ jeu }}</li>
{% endfor %}
</ul>
{% endblock %}
Fichier static/style.css :
body { font-family: Arial, sans-serif; margin: 0; background: #f4f6fb; color: #222; }
nav { background: #2a4d8f; padding: 12px 24px; }
nav a { color: white; margin-right: 18px; text-decoration: none; font-weight: bold; }
main { max-width: 640px; margin: 24px auto; padding: 0 16px; }
h1 { color: #2a4d8f; }
Fichier app.py :
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def accueil():
return render_template("accueil.html")
@app.route("/jeux")
def jeux():
return render_template("jeux.html", jeux=["Mots cachés", "Snake", "2048"])
if __name__ == "__main__":
app.run(debug=True)

La page /jeux : le menu et le style viennent de base.html.
{% extends "base.html" %}doit être la première instruction du gabarit. Ce qui est écrit après, en dehors des blocs{% block %}, est ignoré ; ce qui est écrit avant est recopié en tête de la page, avant même<!DOCTYPE html>, ce qui la rend incorrecte. Un<link>vers une feuille de style se place donc dansbase.html.url_for('static', filename='style.css')calcule l’adresse du fichier statique :/static/style.css.url_for('jeux')calcule l’adresse de la vuejeux(on donne le nom de la fonction, pas l’adresse) : si l’on change un jour la route, tous les liens suivent.
Les formulaires
Un formulaire HTML envoie des données au serveur ; le cours IHM Web
présente les deux méthodes, GET et POST. Dans Flask, l’objet request contient tout ce que la requête a apporté.
| GET | POST | |
|---|---|---|
| Où sont les données ? | dans l’adresse : /recherche?mot=chat | dans le corps de la requête, invisibles dans l’adresse |
| Lecture dans Flask | request.args.get("mot") | request.form.get("mot") |
| Usage | recherche, filtre : la page peut être mise en favori | envoi qui modifie quelque chose : inscription, réponse, message |
Un formulaire GET : une recherche
Fichier app.py :
from flask import Flask, render_template, request
app = Flask(__name__)
PAYS = {"France": "Paris", "Italie": "Rome", "Espagne": "Madrid",
"Allemagne": "Berlin", "Portugal": "Lisbonne", "Japon": "Tokyo"}
@app.route("/capitales")
def capitales():
mot = request.args.get("mot", "") # "" si le paramètre est absent
trouves = {}
for pays in PAYS:
if mot.lower() in pays.lower():
trouves[pays] = PAYS[pays]
return render_template("capitales.html", mot=mot, trouves=trouves)
if __name__ == "__main__":
app.run(debug=True)
Fichier templates/capitales.html :
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="utf-8"><title>Capitales</title></head>
<body>
<h1>Chercher un pays</h1>
<form action="/capitales" method="get">
<input type="text" name="mot" value="{{ mot }}">
<button type="submit">Chercher</button>
</form>
<ul>
{% for pays, capitale in trouves.items() %}
<li>{{ pays }} : {{ capitale }}</li>
{% else %}
<li>Aucun pays trouvé.</li>
{% endfor %}
</ul>
</body>
</html>

L’adresse contient la recherche : ?mot=al.
L’attribut name="mot" du champ donne le nom du paramètre ; c’est ce nom qu’on lit avec request.args.get("mot"). Le second argument de get est la valeur renvoyée quand le paramètre est absent, à la première visite. Dans le gabarit, {% else %} placé dans une boucle for s’affiche quand la boucle n’a rien à parcourir.
Question. Quelle adresse le navigateur demande-t-il si l’on tape pagne dans le champ et qu’on clique sur « Chercher » ? Quel résultat s’affiche ?
Réponse
/capitales?mot=pagne : avec la méthode GET, les données du formulaire sont ajoutées à l’adresse après un point d’interrogation. Le résultat est « Espagne : Madrid », seul pays dont le nom contient pagne.
Un formulaire POST : la même adresse pour afficher et pour recevoir
Une vue peut accepter les deux méthodes : GET pour afficher le formulaire vide, POST pour traiter ce qu’on a envoyé. On le précise avec methods=["GET", "POST"], et request.method indique la méthode de la requête en cours.
Fichier app.py :
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route("/imc", methods=["GET", "POST"])
def imc():
message = ""
if request.method == "POST":
try:
masse = float(request.form["masse"].replace(",", "."))
taille = float(request.form["taille"].replace(",", "."))
message = f"IMC : {masse / taille ** 2:.1f}"
except (ValueError, ZeroDivisionError):
message = "Saisir deux nombres, la taille n'étant pas nulle."
return render_template("imc.html", message=message)
if __name__ == "__main__":
app.run(debug=True)
Fichier templates/imc.html :
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="utf-8"><title>IMC</title></head>
<body>
<h1>Indice de masse corporelle</h1>
<form method="post">
<p><label>Masse (kg) : <input type="text" name="masse"></label></p>
<p><label>Taille (m) : <input type="text" name="taille"></label></p>
<button type="submit">Calculer</button>
</form>
<p><strong>{{ message }}</strong></p>
</body>
</html>

Après l’envoi du formulaire avec 62 et 1,70.
C’est le même calcul que dans la page Tkinter
: seule l’interface change. Les données d’un formulaire arrivent toujours sous forme de chaînes, comme input() ou Entry.get() : on les convertit, et on prévoit le cas où la conversion échoue.
Sans methods=["GET", "POST"], la route n’accepte que GET, et l’envoi du formulaire produit l’erreur 405 Method Not Allowed.
Garder une information d’une page à l’autre : la session
HTTP est un protocole sans mémoire : chaque requête est indépendante, et le serveur ne sait pas que deux requêtes viennent du même joueur. Une variable Python ordinaire ne convient pas non plus : elle serait partagée par tous les visiteurs du site.
Flask fournit pour cela la session : un dictionnaire propre à chaque visiteur, session, que Flask range dans un cookie
du navigateur. Ce cookie est signé avec une clé secrète, app.secret_key : le visiteur peut le lire, mais pas le modifier sans que le serveur s’en aperçoive.
Le jeu du nombre mystère illustre tout ce qui précède : le nombre à deviner et le nombre d’essais sont gardés dans la session.
Fichier app.py :
import random
from flask import Flask, redirect, render_template, request, session, url_for
app = Flask(__name__)
app.secret_key = "une phrase longue et difficile à deviner"
@app.route("/")
def accueil():
"""Commence une nouvelle partie."""
session["secret"] = random.randint(1, 100)
session["essais"] = 0
session["message"] = "J'ai choisi un nombre entre 1 et 100."
return redirect(url_for("jouer"))
@app.route("/jouer", methods=["GET", "POST"])
def jouer():
if "secret" not in session: # pas de partie en cours
return redirect(url_for("accueil"))
if request.method == "POST":
proposition = request.form.get("proposition", "")
if not proposition.isdigit():
session["message"] = "Il faut taper un nombre entier."
else:
n = int(proposition)
session["essais"] = session["essais"] + 1
if n < session["secret"]:
session["message"] = f"{n} : c'est plus."
elif n > session["secret"]:
session["message"] = f"{n} : c'est moins."
else:
session["message"] = f"Bravo, trouvé en {session['essais']} essai(s) !"
return redirect(url_for("jouer")) # on revient en GET
return render_template("jouer.html", message=session["message"], essais=session["essais"])
if __name__ == "__main__":
app.run(debug=True)
Fichier templates/jouer.html :
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="utf-8"><title>Nombre mystère</title></head>
<body>
<h1>Nombre mystère</h1>
<p>{{ message }}</p>
<p>Essais : {{ essais }}</p>
<form method="post">
<input type="text" name="proposition" autofocus>
<button type="submit">Proposer</button>
</form>
<p><a href="{{ url_for('accueil') }}">Nouvelle partie</a></p>
</body>
</html>

Après trois propositions : 50, 25, puis 37.
Le déroulement d’une partie montre le rôle de chaque morceau :
| Requête | Vue | Ce qui se passe | Réponse |
|---|---|---|---|
GET / | accueil | tirage du nombre, essais à 0, rangés dans la session | redirection vers /jouer |
GET /jouer | jouer | lecture du message et des essais dans la session | page du jeu |
POST /jouer avec proposition=50 | jouer | comparaison, essais à 1, nouveau message dans la session | redirection vers /jouer |
GET /jouer | jouer | lecture de la session | page avec « 50 : c’est moins. » |
redirect(url_for("jouer"))répond au navigateur « va plutôt à l’adresse/jouer» (code HTTP 302) ; le navigateur fait aussitôt une nouvelle requête GET. Rediriger après un POST évite qu’une actualisation de la page renvoie la même proposition une seconde fois.- Sans
app.secret_key, la première utilisation desessionprovoque une erreur : Flask refuse de créer un cookie qu’il ne peut pas signer.
Question. Deux élèves jouent en même temps sur le même serveur. Ont-ils le même nombre mystère ? Et si l’on avait rangé le nombre dans une variable globale secret ?
Réponse
Avec la session, chacun a son propre nombre : il est rangé dans le cookie de son navigateur. Avec une variable globale, il n’y aurait qu’un nombre pour tout le serveur : chaque nouvelle partie lancée par l’un changerait le nombre de l’autre, et les compteurs d’essais se mélangeraient.
Lire des données dans un fichier
Un site lit souvent ses données dans un fichier : liste de mots, questions d’un quiz, scores. Comme pour les images de Pygame, le chemin doit être construit à partir de l’emplacement du programme, sinon le site ne fonctionne que si on le lance depuis son dossier, ou que sur l’ordinateur de son auteur :
import json
import os
DOSSIER = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(DOSSIER, "donnees", "mots.json"), encoding="utf-8") as f:
MOTS = json.load(f) # chargé une fois, au démarrage du serveur
Un chemin comme /workspaces/mon-projet/donnees/mots.json ou C:/Users/Lina/Bureau/donnees/mots.json ne fonctionne que sur la machine où il a été écrit.
Renvoyer des données plutôt qu’une page
Une vue peut renvoyer des données au format JSON au lieu d’une page HTML, grâce à jsonify. Un programme JavaScript de la page les demande alors avec fetch et met la page à jour sans la recharger (voir le cours JavaScript
) :
from flask import jsonify
@app.route("/api/score")
def api_score():
return jsonify({"joueur": "Lina", "score": 42})
fetch("/api/score")
.then(reponse => reponse.json())
.then(donnees => {
document.getElementById("score").textContent = donnees.score;
});
Qui fait quoi : le serveur et le navigateur
Un site Flask fait travailler deux programmes : le serveur, lancé par app.py, et le navigateur, qui affiche les pages. Pendant le développement, ils tournent sur le même ordinateur ; une fois le site publié, le serveur tourne chez un hébergeur et le navigateur chez chaque visiteur. La partie qui s’exécute sur le serveur s’appelle la partie serveur (en anglais back end), celle qui s’exécute dans le navigateur la partie client (front end).
| Fichier | Langage | Où il s’exécute | Partie |
|---|---|---|---|
app.py | Python | serveur | serveur |
templates/page.html | HTML et Jinja | serveur, puis navigateur | les deux |
static/style.css | CSS | navigateur | client |
static/script.js | JavaScript | navigateur | client |
Les gabarits sont un cas mixte : Jinja est rempli sur le serveur, puis le HTML obtenu est affiché par le navigateur.
Flask ne comprend pas le JavaScript : pour lui, script.js n’est que du texte, comme une image. Il l’envoie sans le lire, comme un facteur apporte une lettre écrite dans une langue qu’il ne parle pas ; c’est le navigateur, qui contient son propre moteur JavaScript, qui l’exécute. À l’inverse, le navigateur ne reçoit jamais le code Python : il ne voit que ce que les vues renvoient.
Avec l’exemple du score de la section précédente, une visite se déroule ainsi :
- Le navigateur demande
/; Flask, en Python, remplit le gabarit et renvoie le HTML. - Le HTML demande
static/script.js; Flask l’envoie tel quel. - Le navigateur exécute
script.js, qui appellefetch("/api/score"). - Flask exécute la vue
api_scoreet renvoie le JSON. - Le JavaScript, dans le navigateur, écrit le score dans la page.
Les deux programmes n’exécutent jamais le code l’un de l’autre : ils échangent seulement des requêtes et des réponses.
Question. Dans la vue notes de la section gabarits
, on ajoute la ligne print(moyenne). Où s’affiche ce nombre ?
Réponse
Dans le terminal où tourne le serveur (celui de Thonny, par exemple), et non dans la page : print s’exécute sur le serveur. Seul ce que la vue renvoie part vers le navigateur.
Question. Un élève écrit {{ moyenne }} dans static/script.js pour afficher la moyenne. Que se passe-t-il ?
Réponse
Jinja ne remplit que les gabarits du dossier templates : les fichiers de static partent tels quels. Le navigateur reçoit donc le texte {{ moyenne }}, qu’il lit comme du JavaScript ; aucune variable moyenne n’y existe, et la console du navigateur affiche l’erreur moyenne is not defined. Pour transmettre une valeur au JavaScript, on la demande avec fetch à une vue qui renvoie du JSON.
Publier son site
Cette section dépasse le programme : elle sert à mettre un projet en ligne pour le montrer.
Avec app.run(), le site n’est visible que sur l’ordinateur qui le lance : 127.0.0.1 désigne cet ordinateur lui-même. Pour le publier, il faut une machine allumée en permanence et joignable depuis Internet ; on la loue à un hébergeur. PythonAnywhere et Render proposent une formule gratuite pour un petit site, avec des limites : chez PythonAnywhere, le site doit être réactivé chaque mois ; chez Render, il s’endort après un quart d’heure sans visite, et la première page suivante met quelques secondes à venir.
Le serveur lancé par app.run() ne convient pas en ligne : il est fait pour une seule personne qui développe. Sur l’hébergeur, on confie l’accueil des visiteurs à un serveur prévu pour cela, comme gunicorn : il lance plusieurs copies de l’application et répartit les requêtes entre elles, si bien que plusieurs visiteurs sont servis en même temps. Le code Flask ne change pas : Flask reste le cuisinier qui prépare les pages, gunicorn devient le maître d’hôtel qui place les visiteurs.
On fournit à l’hébergeur trois choses :
- le dossier du projet, avec
app.py,templatesetstatic, le plus souvent par un dépôt GitHub ; - un fichier
requirements.txt, la liste des bibliothèques qu’il doit installer ; - la commande de démarrage,
gunicorn app:app, qui se lit « dans le fichierapp.py, l’objetapp».
Fichier requirements.txt :
flask
gunicorn
En ligne, debug=True ne doit jamais être actif : sa page d’erreur montre le code du serveur à n’importe quel visiteur. La clé app.secret_key ne doit pas non plus être publiée avec le code : quelqu’un qui la connaît peut fabriquer de fausses sessions.
Comptes et mots de passe
Un site avec des comptes ne doit jamais enregistrer les mots de passe en clair : si le fichier ou la base de données est volé, tous les mots de passe le sont aussi. On enregistre une empreinte (hash) du mot de passe, calculée par une fonction à sens unique : on ne peut pas retrouver le mot de passe à partir de l’empreinte, mais on peut vérifier qu’un mot de passe tapé lui correspond. La bibliothèque Werkzeug, installée avec Flask, fournit ces deux fonctions :
from werkzeug.security import check_password_hash, generate_password_hash
empreinte = generate_password_hash("motdepasse") # à l'inscription : on enregistre empreinte
check_password_hash(empreinte, "motdepasse") # à la connexion : True
check_password_hash(empreinte, "autre") # False
Une fois l’utilisateur connecté, on range son nom dans la session (session["utilisateur"] = nom), et chaque vue réservée vérifie sa présence.
Erreurs fréquentes
| Symptôme | Cause | Correction |
|---|---|---|
jinja2.exceptions.TemplateNotFound: notes.html | le gabarit n’est pas dans un dossier nommé exactement templates, à côté de app.py | vérifier le nom et l’emplacement du dossier |
| la feuille de style n’est pas appliquée | fichier hors du dossier static, ou <link> placé hors des blocs d’une page fille | static/style.css, url_for('static', filename='style.css') dans base.html |
| 404 Not Found | aucune route pour cette adresse, ou faute de frappe | comparer l’adresse et le @app.route |
| 405 Method Not Allowed | formulaire POST envoyé à une route qui n’accepte que GET | methods=["GET", "POST"] |
BadRequestKeyError (400 Bad Request), KeyError: 'nom' | request.form["nom"] alors que le formulaire n’a pas de champ name="nom" | accorder les noms, ou request.form.get("nom", "") |
RuntimeError: The session is unavailable because no secret key was set. | app.secret_key absent | définir app.secret_key |
| une variable n’apparaît pas dans la page | elle n’est pas transmise à render_template | render_template("page.html", nom=valeur) |
Address already in use, ou page « Access denied » sur le port 5000 | un autre programme (un autre serveur, ou un service du système) occupe le port 5000 | arrêter l’autre serveur, ou app.run(debug=True, port=5001) |
FileNotFoundError au démarrage | chemin relatif au dossier courant, ou chemin absolu d’une autre machine | os.path.join(DOSSIER, ...) |
| le serveur exécute du code de test au démarrage, ou des erreurs d’import en boucle | app.py importe un fichier qui l’importe à son tour, ou du code hors de if __name__ == "__main__": | les fonctions dans un module, le code de test dans le bloc if __name__ == "__main__": |
| les modifications ne s’affichent pas | serveur lancé sans debug=True, ou ancienne version en cache | relancer le serveur, actualiser la page |
Pour aller plus loin
- Ajouter au nombre mystère un meilleur score (le plus petit nombre d’essais), gardé dans la session.
- Faire un quiz : les questions dans un fichier JSON, la question en cours et le score dans la session, une page de résultat à la fin.
- Reprendre le site de la section mise en page commune avec une page par projet de la classe.
- La documentation de Flask (en anglais), et en particulier son tutoriel.
À retenir
- Flask est une bibliothèque Python pour écrire un serveur web : à chaque adresse correspond une vue, une fonction dont la valeur renvoyée devient la page.
@app.route("/adresse")associe une adresse à une vue ;<nom>ou<int:nom>dans la route transmet une partie de l’adresse en paramètre.- Le serveur se lance avec
app.run(debug=True)dans un blocif __name__ == "__main__":; le site se visite à l’adressehttp://127.0.0.1:5000. - Les gabarits, dans
templates/, sont du HTML enrichi de Jinja :{{ valeur }},{% for %},{% if %};render_templateleur transmet les variables. {% extends "base.html" %}et{% block %}donnent une mise en page commune ; CSS, images et scripts vont dansstatic/.request.argscontient les données d’un formulaire GET,request.formcelles d’un formulaire POST ; elles arrivent toujours sous forme de chaînes.- La session, signée avec
app.secret_key, garde des informations propres à chaque visiteur d’une requête à l’autre. redirect(url_for("vue"))renvoie le navigateur vers une autre vue ; on redirige après le traitement d’un POST.- Python et Jinja s’exécutent sur le serveur (partie serveur, back end) ; HTML, CSS et JavaScript dans le navigateur (partie client, front end). Flask envoie les fichiers de
staticsans les exécuter. app.run()est un serveur de développement ; en ligne, un hébergeur fait tourner l’application avec un serveur comme gunicorn, d’aprèsrequirements.txtet la commandegunicorn app:app.
Résumé
| Concept | Syntaxe | Exemple |
|---|---|---|
| Application | app = Flask(__name__) | une par site |
| Vue | @app.route("/adresse") puis def vue(): | @app.route("/jeux") |
| Paramètre d’adresse | <nom>, <int:nom> | @app.route("/table/<int:n>") |
| Lancer | app.run(debug=True) | http://127.0.0.1:5000 |
| Gabarit | render_template("page.html", x=valeur) | dossier templates/ |
| Insérer, répéter, choisir | {{ x }}, {% for %}, {% if %} | {{ eleve }} |
| Hériter | {% extends "base.html" %}, {% block nom %} | menu commun |
| Adresse d’une vue ou d’un fichier | url_for("vue"), url_for("static", filename=...) | href="{{ url_for('accueil') }}" |
| Formulaire GET | request.args.get("nom", "") | recherche |
| Formulaire POST | methods=["GET", "POST"], request.form["nom"] | envoi d’une réponse |
| Session | app.secret_key, session["cle"] | score d’un joueur |
| Redirection | redirect(url_for("vue")) | après un POST |
| Données JSON | jsonify(dictionnaire) | fetch en JavaScript |
| Publier | requirements.txt, gunicorn app:app | chez un hébergeur |