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.
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.
Flask s’installe une fois sur l’ordinateur :
flask et cliquer sur Installer ;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.1 est l’adresse IP de l’ordinateur lui-même (le nom localhost désigne cette même adresse) : le client et le serveur sont ici sur la même machine. 5000 est 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.debug=True, le serveur redémarre tout seul quand on enregistre app.py, et une erreur affiche une page d’explication dans le navigateur. Ce mode ne sert qu’au développement, sur son propre ordinateur.Ctrl + C dans le terminal, ou le bouton Stop de Thonny.Question. Que renvoie le serveur si l’on visite http://127.0.0.1:5000/contact ?
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.
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.
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 ?
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().
É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) ?
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.
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 dans base.html.url_for('static', filename='style.css') calcule l’adresse du fichier statique : /static/style.css.url_for('jeux') calcule l’adresse de la vue jeux (on donne le nom de la fonction, pas l’adresse) : si l’on change un jour la route, tous les liens suivent.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 |
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 ?
/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.
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.
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.app.secret_key, la première utilisation de session provoque 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 ?
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.
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.
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;
});
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 :
/ ; Flask, en Python, remplit le gabarit et renvoie le HTML.static/script.js ; Flask l’envoie tel quel.script.js, qui appelle fetch("/api/score").api_score et renvoie le JSON.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 ?
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 ?
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.
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 :
app.py, templates et static, le plus souvent par un dépôt GitHub ;requirements.txt, la liste des bibliothèques qu’il doit installer ;gunicorn app:app, qui se lit « dans le fichier app.py, l’objet app ».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.
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.
| 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"] |
| Symptôme | Cause | Correction |
|---|---|---|
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) |
| Symptôme | Cause | Correction |
|---|---|---|
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 |
@app.route("/adresse") associe une adresse à une vue ; <nom> ou <int:nom> dans la route transmet une partie de l’adresse en paramètre.app.run(debug=True) dans un bloc if __name__ == "__main__": ; le site se visite à l’adresse http://127.0.0.1:5000.templates/, sont du HTML enrichi de Jinja : {{ valeur }}, {% for %}, {% if %} ; render_template leur transmet les variables.{% extends "base.html" %} et {% block %} donnent une mise en page commune ; CSS, images et scripts vont dans static/.request.args contient les données d’un formulaire GET, request.form celles d’un formulaire POST ; elles arrivent toujours sous forme de chaînes.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.static sans 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ès requirements.txt et la commande gunicorn app:app.| 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 |
| Concept | Syntaxe | Exemple |
|---|---|---|
| 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 |