Aller au contenu principal

Module 6 — État de session et formulaires

Deux limites gênent l'application au sortir des cinq premiers modules. Un score calculé après un clic disparaît dès qu'un autre composant bouge, parce que le script est réexécuté du haut en bas. Et chaque petit changement d'un curseur relance immédiatement tous les calculs, même quand l'utilisateur voudrait ajuster plusieurs champs avant d'appuyer sur « Calculer ». Ce module résout ces deux problèmes avec st.session_state et st.form.

st.session_state, la mémoire de la session

L'état de session est un dictionnaire spécial, propre à chaque onglet ouvert dans le navigateur, qui survit à toutes les réexécutions du script. Streamlit y stocke automatiquement la valeur courante de chaque composant identifié par une clé ; on peut aussi y écrire manuellement.

import streamlit as st

# Initialisation défensive : la clé peut ne pas exister au premier passage.
if "compteur" not in st.session_state:
st.session_state.compteur = 0

if st.button("Incrémenter"):
st.session_state.compteur += 1

st.write(f"Compteur : {st.session_state.compteur}")

Cette fois, le compteur augmente vraiment. Le session_state.compteur survit d'une exécution à l'autre parce qu'il vit dans la session, pas dans la portée locale du script. On accède aux clés indifféremment par attribut (st.session_state.compteur) ou par indexation (st.session_state["compteur"]) ; les deux formes sont interchangeables et parfaitement équivalentes.

La règle d'initialisation défensive est essentielle. Au tout premier passage, la clé n'existe pas ; y accéder directement lève une exception. On protège chaque clé par un test d'existence ou par st.session_state.setdefault("compteur", 0), exactement comme pour un dictionnaire ordinaire.

Conserver un résultat entre exécutions

Reprenons le formulaire de scoring du module 2 : un score s'affiche au clic sur le bouton, puis disparaît dès que l'utilisateur touche un curseur. La correction tient en trois lignes.

import streamlit as st

st.title("Score de résiliation — Un client")

anciennete = st.number_input("Ancienneté (mois)", 0, 120, 24)
mensualite = st.slider("Mensualité (EUR)", 10.0, 150.0, 45.0)
type_contrat = st.selectbox("Contrat", ["Mensuel", "Annuel", "Deux ans"])

def scorer(a, m, c):
base = 0.10 + 0.005 * m - 0.008 * a
return max(0.0, min(1.0, base + (0.15 if c == "Mensuel" else 0)))

if st.button("Calculer le score"):
st.session_state.score = scorer(anciennete, mensualite, type_contrat)
st.session_state.dernier_score = {
"anciennete": anciennete,
"mensualite": mensualite,
"contrat": type_contrat,
}

if "score" in st.session_state:
st.metric("Probabilité de résiliation", f"{st.session_state.score:.0%}")
st.caption(
f"Calculé pour un client de {st.session_state.dernier_score['anciennete']} mois "
f"d'ancienneté à {st.session_state.dernier_score['mensualite']:.2f} EUR."
)

Deux points valent la peine d'être notés. On stocke non seulement le score mais le contexte dans lequel il a été calculé, ce qui évite la confusion classique d'un score affiché à côté de valeurs modifiées depuis. Et l'affichage du score est conditionnel à la présence de la clé, pas au clic sur le bouton, ce qui découple l'affichage de l'événement de calcul.

Les rappels de composants

Chaque composant accepte un argument on_change (ou on_click pour un bouton) qui pointe vers une fonction Python appelée avant la réexécution du script. C'est le mécanisme des mises à jour croisées : quand un composant change, un rappel modifie l'état, puis la réexécution normale prend en compte le nouvel état.

import streamlit as st

def basculer_theme():
st.session_state.theme = "sombre" if st.session_state.theme == "clair" else "clair"

if "theme" not in st.session_state:
st.session_state.theme = "clair"

st.button("Basculer le thème", on_click=basculer_theme)
st.write(f"Thème actuel : {st.session_state.theme}")

Le rappel est particulièrement utile pour synchroniser des composants dépendants — une liste déroulante qui met à jour les options d'une autre, par exemple. La fonction reçoit implicitement l'ensemble de l'état de session, mais on peut aussi passer explicitement des arguments avec args et kwargs.

Les formulaires, l'exception au modèle d'exécution

Le modèle de réexécution complète décrit au module 1 pose un problème quand un utilisateur veut ajuster plusieurs champs avant de valider. Sans précaution, chaque changement d'un champ déclenche une réexécution. La solution est st.form : les composants placés à l'intérieur ne déclenchent aucune réexécution tant que le bouton de soumission n'a pas été pressé.

import streamlit as st

with st.form("scoring", clear_on_submit=False):
st.subheader("Caractéristiques du client")
col_a, col_b = st.columns(2)
with col_a:
anciennete = st.number_input("Ancienneté (mois)", 0, 120, 24)
mensualite = st.slider("Mensualité (EUR)", 10.0, 150.0, 45.0)
with col_b:
contrat = st.selectbox("Contrat", ["Mensuel", "Annuel", "Deux ans"])
fibre = st.checkbox("Fibre optique")

valide = st.form_submit_button("Calculer le score", type="primary")

if valide:
st.session_state.score = scorer(anciennete, mensualite, contrat)
st.metric("Probabilité", f"{st.session_state.score:.0%}")

Trois règles à connaître. Un formulaire doit contenir au moins un st.form_submit_button, sans quoi Streamlit lève une erreur explicite. clear_on_submit=True réinitialise le formulaire après validation, utile pour une saisie répétée ; clear_on_submit=False conserve les dernières valeurs, utile pour un scoring qu'on veut ajuster. Un formulaire ne peut pas contenir un bouton ordinaire — seul le bouton de soumission a un sens — et il ne peut pas être imbriqué dans un autre formulaire.

Une navigation par étapes

L'état de session permet d'implémenter une navigation par étapes, courante dans les assistants de configuration. Chaque étape est une vue distincte, activée par une clé d'état.

import streamlit as st

etapes = ["client", "offre", "validation"]
if "etape" not in st.session_state:
st.session_state.etape = "client"

st.progress((etapes.index(st.session_state.etape) + 1) / len(etapes))

if st.session_state.etape == "client":
st.header("Étape 1 — Client")
nom = st.text_input("Nom du client", key="nom_client")
if st.button("Suivant") and nom:
st.session_state.etape = "offre"
st.rerun()

elif st.session_state.etape == "offre":
st.header("Étape 2 — Offre proposée")
offre = st.selectbox("Offre", ["Standard", "Premium", "Fidélité"])
col_p, col_s = st.columns(2)
if col_p.button("Précédent"):
st.session_state.etape = "client"
st.rerun()
if col_s.button("Suivant"):
st.session_state.offre = offre
st.session_state.etape = "validation"
st.rerun()

else:
st.header("Étape 3 — Validation")
st.success(f"Offre {st.session_state.offre} envoyée à {st.session_state.nom_client}.")

st.rerun() force la réexécution immédiate, sans quoi la nouvelle étape ne s'afficherait qu'au prochain clic. La barre de progression donne un repère visuel utile ; sans elle, l'utilisateur perd le fil dès la deuxième étape.

La double vie de session_state

Une même clé peut servir de source pour un composant et être écrite ailleurs, mais jamais dans la même exécution. Écrire st.session_state.mensualite = 60.0 juste avant un st.slider("Mensualité", key="mensualite") lève une erreur. La règle : soit la clé est écrite par un rappel on_change, soit elle est écrite dans une exécution différente, jamais les deux dans le même passage. Cette contrainte protège contre les boucles infinies d'écriture et de relecture.

En résumé

  • st.session_state est un dictionnaire propre à la session, qui survit aux réexécutions du script ; il est indispensable dès qu'un résultat doit persister au-delà d'un clic.
  • Les rappels on_change et on_click s'exécutent avant la réexécution du script, ce qui permet des mises à jour croisées entre composants.
  • st.form groupe des composants dont les changements ne déclenchent la réexécution qu'à la validation du bouton de soumission ; c'est l'exception voulue au modèle d'exécution complète.
  • Une navigation par étapes s'implémente en stockant l'étape courante dans session_state et en appelant st.rerun() pour rafraîchir immédiatement l'affichage.

Module suivant : le téléversement de fichiers, pour scorer un CSV de clients en lot plutôt qu'un par un.