Les 10 Erreurs qui Ralentissent vos Applications Streamlit (et comment les corriger)
Du Prototype à la Production : L'Art d'Éviter les Pièges Courants en Streamlit
Streamlit a transformé la manière dont nous, analystes et développeurs Python, créons des applications de données. Sa simplicité nous permet de passer d'un script à une interface web interactive en quelques minutes. Cependant, ce passage rapide du prototype à une application fonctionnelle cache souvent des problèmes de performance, de robustesse et de sécurité.
Après avoir accompagné des dizaines d'entreprises, des banques aux scale-ups, et audité plus de 50 applications Streamlit, j'ai constaté que les mêmes erreurs revenaient systématiquement. Ces erreurs, bien qu'invisibles au premier abord, sont celles qui ralentissent vos applications, frustrent vos utilisateurs et, dans le pire des cas, provoquent des pannes en production.
Ce guide n'est pas une simple liste. C'est un transfert d'expérience, conçu pour vous faire économiser d'innombrables heures de débogage. Nous allons disséquer ensemble les 10 erreurs les plus fréquentes et, plus important encore, nous verrons comment les corriger pour bâtir des applications non seulement fonctionnelles, mais aussi stables, rapides et professionnelles.
1. Le Caching : Le Réflexe Fondamental pour une Application Réactive
Pour bien comprendre l'importance du caching, il faut d'abord saisir le fonctionnement de Streamlit. À chaque interaction d'un utilisateur — un clic sur un bouton, une sélection dans un menu déroulant — Streamlit ré-exécute l'intégralité de votre script, du haut vers le bas. Ce modèle est la clé de sa simplicité, mais il est aussi son plus grand piège en matière de performance.
Imaginez une fonction qui charge un jeu de données volumineux. Sans mécanisme de mise en cache, ce chargement se répète à chaque interaction, même si les données n'ont pas changé.
Observons un exemple typique de ce comportement. Le code ci-dessous définit une fonction load_data qui lit un fichier CSV potentiellement lourd.
import streamlit as st
import pandas as pd
# ❌ MAUVAIS - Rechargé à CHAQUE interaction !
def load_data():
df = pd.read_csv('huge_dataset.csv') # 500 MB
return df
df = load_data()
# Chaque fois qu'un utilisateur clique sur un bouton...
# Le fichier est rechargé ! 🐌
if st.button("Filtrer"):
filtered = df[df['category'] == 'A']
st.write(filtered)
Le résultat ? Une application qui semble fonctionner, mais qui est terriblement lente. Chaque clic de l'utilisateur déclenche une attente de plusieurs secondes, pendant laquelle le message "Running..." s'affiche en permanence. C'est l'une des premières causes de frustration pour l'utilisateur final et un signe clair que l'application n'est pas optimisée.
La Solution : Mémoriser les Calculs avec les Décorateurs de Cache
La solution est aussi élégante que puissante : les décorateurs de cache de Streamlit. En ajoutant simplement une ligne au-dessus de votre fonction, vous indiquez à Streamlit de ne la ré-exécuter que si nécessaire.
Reprenons notre exemple, cette fois-ci en utilisant @st.cache_data.
import streamlit as st
import pandas as pd
# ✅ BON - Chargé UNE SEULE FOIS !
@st.cache_data
def load_data():
df = pd.read_csv('huge_dataset.csv')
return df
df = load_data() # Rapide après le premier chargement
if st.button("Filtrer"):
filtered = df[df['category'] == 'A']
st.write(filtered)
La première fois que load_data est appelée, Streamlit exécute la fonction et stocke le résultat (le DataFrame) dans un cache. Pour tous les appels suivants, au lieu de ré-exécuter la fonction, Streamlit renvoie instantanément le résultat stocké.
Streamlit propose deux décorateurs principaux pour des besoins différents. Utilisez @st.cache_data pour tout ce qui est "donnée" : des DataFrames Pandas, des listes, des dictionnaires, ou des sorties d'API au format JSON. C'est le plus courant. Réservez @st.cache_resource pour les "ressources" qui ne peuvent pas être facilement stockées comme des données, telles que les connexions à une base de données, les clients API, ou les modèles de machine learning chargés en mémoire.
Par exemple, vous structureriez votre code ainsi :
# Pour les DataFrames
@st.cache_data
def load_csv():
return pd.read_csv('data.csv')
# Pour les connexions de base de données
@st.cache_resource
def get_database_connection():
return psycopg2.connect(...)
# Pour les modèles ML
@st.cache_resource
def load_model():
return joblib.load('model.pkl')
Il est même possible d'affiner le comportement du cache. Vous pouvez par exemple définir une durée de vie (TTL, ou "Time To Live") pour rafraîchir automatiquement les données après un certain temps, ou afficher un message de chargement pour améliorer l'expérience utilisateur pendant l'exécution initiale d'une fonction longue.
# Rafraîchir le cache toutes les heures
@st.cache_data(ttl=3600)
def load_live_data():
return fetch_from_api()
# Afficher un spinner pendant le chargement initial
@st.cache_data(show_spinner="Chargement des données en cours...")
def load_data():
return pd.read_csv('data.csv')
L'impact de cette seule optimisation est considérable. Sur les applications que j'ai pu optimiser, l'implémentation correcte du caching a permis de réduire les temps de chargement de 80 à 95%. C'est la différence entre une application inutilisable et une application professionnelle.
2. La Maîtrise du Rerun : Exécuter Uniquement le Nécessaire
Nous avons établi que Streamlit ré-exécute le script à chaque interaction. Si le caching est la première ligne de défense pour les fonctions de chargement de données, que se passe-t-il lorsque des calculs lourds sont effectués directement dans le corps du script ? La réponse est simple : ils sont ré-exécutés inutilement, encore et encore.
C'est une erreur subtile mais aux conséquences désastreuses pour l'expérience utilisateur. Le script suivant illustre ce problème : un calcul long (simulé ici par time.sleep(2)) est placé directement dans le flux d'exécution. Juste en dessous, un simple champ de texte attend le nom de l'utilisateur.
import streamlit as st
import time
# ❌ MAUVAIS - Exécuté à CHAQUE rerun
st.title("Mon Dashboard")
# Calcul très lourd qui n'a rien à voir avec l'interaction
time.sleep(2) # Simule un calcul long
heavy_computation = sum(range(10_000_000))
# Widget qui déclenche un rerun
name = st.text_input("Votre nom")
st.write(f"Bonjour {name}")
# À chaque lettre tapée → rerun complet → 2 secondes d'attente !
Le résultat est une application qui "gèle" à chaque interaction. Pour chaque lettre que l'utilisateur tape dans le champ de texte, l'application entière se met en pause pendant deux secondes. Le processeur tourne à plein régime et l'expérience devient si frustrante que l'application en devient inutilisable.
Les Stratégies pour Contrôler les Exécutions
Heureusement, nous disposons d'une palette d'outils pour maîtriser ce comportement. Le choix de l'outil dépendra du contexte.
Stratégie 1 : Le Caching (Encore lui)
La solution la plus directe, si le calcul peut être isolé dans une fonction, est d'appliquer le même principe de caching que nous avons vu précédemment. En encapsulant le calcul lourd dans une fonction décorée avec @st.cache_data, nous nous assurons qu'il ne sera exécuté qu'une seule fois.
import streamlit as st
import time
@st.cache_data # ✅ Exécuté une seule fois !
def heavy_computation():
time.sleep(2)
return sum(range(10_000_000))
st.title("Mon Dashboard")
result = heavy_computation()
name = st.text_input("Votre nom")
st.write(f"Bonjour {name}")
Stratégie 2 : L'État de Session pour les Opérations Uniques
Parfois, un calcul ne doit être effectué qu'une seule fois par session utilisateur, et non pas seulement lorsque ses entrées changent. Dans ce cas, st.session_state est l'outil idéal. On l'utilise comme un drapeau pour vérifier si le calcul a déjà été effectué.
import streamlit as st
# Initialiser une seule fois au début de la session
if 'computed' not in st.session_state:
st.session_state.computed = False
if not st.session_state.computed:
# Ce bloc ne s'exécute qu'une seule fois
st.session_state.result = sum(range(10_000_000))
st.session_state.computed = True
# Le résultat est ensuite simplement ré-affiché à chaque rerun
st.write(f"Résultat : {st.session_state.result}")
Stratégie 3 : Les Formulaires pour Grouper les Actions Utilisateur
La cause la plus fréquente de reruns inutiles provient des widgets de saisie. Pour éviter qu'un rerun ne soit déclenché à chaque caractère tapé ou chaque case cochée, on peut grouper plusieurs widgets dans un formulaire avec st.form. Le script ne sera alors ré-exécuté que lorsque l'utilisateur clique explicitement sur le bouton de soumission du formulaire.
import streamlit as st
# ✅ BON - Un seul rerun quand on soumet le formulaire
with st.form("my_form"):
st.write("Veuillez remplir les champs ci-dessous")
name = st.text_input("Nom")
age = st.number_input("Âge", min_value=0, max_value=120)
city = st.selectbox("Ville", ["Paris", "Lyon", "Marseille"])
# Le bouton de soumission est lié au formulaire
submitted = st.form_submit_button("Valider")
# Le code à l'intérieur de ce `if` ne s'exécute
# que lorsque le formulaire est soumis.
if submitted:
st.write(f"{name}, {age} ans, habite à {city}")
# C'est ici que l'on placerait un traitement lourd
La maîtrise de ces trois techniques est un pas de géant vers des applications professionnelles. Sur un projet client, l'application de ces principes a permis de faire passer le temps de réponse d'une interaction de 15 longues secondes à moins d'une seconde.
3. Le session_state : Gardien de la Mémoire de votre Application
Si le modèle de rerun de Streamlit est sa force, il introduit un défi majeur : comment conserver une information d'une exécution à l'autre ? Par défaut, une application Streamlit est amnésique. Chaque interaction réinitialise toutes les variables. C'est ici qu'intervient st.session_state, l'objet qui donne une mémoire à votre application.
Il s'agit d'un dictionnaire Python accessible à tout moment, qui persiste tout au long d'une session utilisateur. C'est l'outil indispensable pour suivre l'état de l'application : qui est l'utilisateur connecté, quelle page est affichée, le résultat d'un calcul, etc.
Cependant, sa mauvaise utilisation est une source fréquente de bugs. L'erreur la plus classique est de tenter d'accéder à une clé avant qu'elle n'ait été créée. Observez ce code qui implémente un simple compteur.
import streamlit as st
# ❌ MAUVAIS - KeyError si 'counter' n'existe pas !
if st.button("Incrémenter"):
st.session_state.counter += 1 # 💥 CRASH au premier clic
st.write(f"Compteur : {st.session_state.counter}")
Au premier lancement de l'application, st.session_state est vide. Le premier clic sur le bouton tente d'incrémenter une valeur qui n'existe pas, provoquant une KeyError et un crash brutal de l'application. C'est le symptôme d'un état non initialisé.
La Règle d'Or : Toujours Initialiser l'État
Pour éviter ce type de crash, il faut adopter une règle simple : toujours s'assurer qu'une clé existe dans st.session_state avant de l'utiliser. Le pattern le plus courant consiste à vérifier sa présence au début du script.
import streamlit as st
# ✅ BON - Initialisation sécurisée
if 'counter' not in st.session_state:
st.session_state.counter = 0
if st.button("Incrémenter"):
st.session_state.counter += 1
st.write(f"Compteur : {st.session_state.counter}")
Ce simple bloc if garantit que st.session_state.counter est initialisé à 0 lors du premier chargement, et ne sera plus jamais ré-initialisé pour le reste de la session. L'application devient ainsi robuste.
Pour des applications plus complexes avec de nombreuses variables d'état, il peut être judicieux de centraliser cette logique dans une fonction réutilisable pour garder un code propre et lisible.
# Fonction helper pour initialiser proprement
def init_session_state(key, default_value):
if key not in st.session_state:
st.session_state[key] = default_value
# Utilisation au début du script
init_session_state('user', None)
init_session_state('data', pd.DataFrame())
init_session_state('page', 'home')
Aller plus loin avec les Callbacks
La véritable puissance de st.session_state se révèle avec les callbacks. Ce sont des fonctions que vous pouvez déclencher en réponse à un changement sur un widget (par exemple, via le paramètre on_change). Cette approche permet de créer des logiques complexes et de modifier l'état de manière contrôlée, sans polluer le script principal.
Voici un exemple de mini-application de chat qui utilise un callback pour ajouter un message à une liste stockée dans l'état de session.
import streamlit as st
# Initialisation de la liste des messages
if 'messages' not in st.session_state:
st.session_state.messages = []
# Le callback est une fonction qui modifie l'état
def add_message():
msg = st.session_state.input_message
if msg: # S'assurer que le message n'est pas vide
st.session_state.messages.append(msg)
st.session_state.input_message = "" # Vider le champ de saisie
# Le widget de saisie est lié au callback
st.text_input(
"Votre message",
key="input_message",
on_change=add_message
)
# L'affichage est simple et découplé de la logique
for msg in st.session_state.messages:
st.write(f"- {msg}")
Une dernière erreur à éviter est de vouloir synchroniser manuellement la valeur d'un widget avec st.session_state. N'écrivez jamais st.session_state.value = st.slider(...). Streamlit le fait pour vous automatiquement et de manière bien plus fiable. Il suffit de fournir un paramètre key au widget, et sa valeur sera toujours disponible dans st.session_state sous cette même clé.
4. La Gestion des Secrets : Ne Jamais Exposer ses Clés d'Accès
Une application passe du statut de prototype à celui de projet sérieux au moment où elle commence à interagir avec des services externes : bases de données, API, etc. Cette transition introduit une nouvelle responsabilité critique : la gestion des informations d'identification (credentials).
Une erreur fréquente, souvent commise par précipitation, est d'inscrire ces informations sensibles — clés d'API, mots de passe de base de données — directement dans le code. Si l'application fonctionne parfaitement sur votre machine locale, cette pratique est une bombe à retardement sécuritaire.
import streamlit as st
import psycopg2
# ❌ DANGEREUX - Credentials en clair dans le code !
conn = psycopg2.connect(
host="db.example.com",
database="production",
user="admin",
password="super_secret_password_123" # 🚨 Exposé publiquement !
)
Le danger est réel : si ce code est partagé sur un dépôt Git public comme GitHub, vos clés d'accès sont instantanément exposées au monde entier. C'est une faille de sécurité béante qui peut entraîner des corruptions de données, des vols d'information et des coûts financiers importants.
La Solution Professionnelle : secrets.toml
Streamlit propose une solution intégrée, à la fois simple et robuste, pour gérer ce problème : le fichier de secrets. Le principe est de séparer la configuration (les secrets) du code.
Étape 1 : Créer un fichier local pour les secrets
Dans votre projet, créez un dossier .streamlit et, à l'intérieur, un fichier nommé secrets.toml. C'est dans ce fichier que vous stockerez toutes vos informations sensibles.
# .streamlit/secrets.toml
[database]
host = "db.example.com"
database = "production"
user = "admin"
password = "super_secret_password_123"
[api]
openai_key = "sk-..."
stripe_key = "sk_live_..."
Étape 2 : Accéder aux secrets dans le code
Votre code n'a plus besoin de connaître les valeurs des secrets, seulement leur existence. On y accède via le dictionnaire st.secrets.
import streamlit as st
import psycopg2
# ✅ BON - Utilisation de st.secrets
conn = psycopg2.connect(
host=st.secrets["database"]["host"],
database=st.secrets["database"]["database"],
user=st.secrets["database"]["user"],
password=st.secrets["database"]["password"]
)
Étape 3 : Ignorer le fichier de secrets (Crucial)
Pour que ce système soit efficace, vous devez impérativement dire à Git de ne jamais suivre ce fichier. Ajoutez son chemin à votre fichier .gitignore. C'est la garantie que vos secrets ne quitteront jamais votre machine locale.
# .gitignore
.streamlit/secrets.toml # ✅ Ne JAMAIS commiter ce fichier !
Étape 4 : Configurer les secrets en production
Une fois votre application prête à être déployée sur Streamlit Community Cloud, il vous suffit de copier le contenu de votre fichier secrets.toml local et de le coller dans l'interface de gestion des secrets de votre application (dans Settings > Secrets). Streamlit se chargera de rendre l'objet st.secrets disponible à votre application en production, de manière sécurisée.
Pour d'autres plateformes de déploiement (serveurs privés, Docker, etc.), la pratique standard est d'utiliser des variables d'environnement, qui remplissent un rôle similaire. Le code s'adapte alors légèrement pour les lire.
import os
# Pour les apps déployées sur un serveur personnel
db_password = os.environ.get("DB_PASSWORD")
La bonne gestion des secrets est une compétence non négociable pour tout développeur d'applications. C'est un marqueur de professionnalisme et un prérequis absolu pour toute application destinée à être utilisée par d'autres.
5. Anticiper l'Échec : Construire des Applications Robustes
Le monde réel est imprévisible. Une API peut être indisponible, un utilisateur peut téléverser un fichier dans un format incorrect, une source de données peut être corrompue. Une application qui n'est pas préparée à ces imprévus offrira une expérience médiocre : elle plantera, affichant à l'utilisateur un écran d'erreur rouge et cryptique qui brise instantanément la confiance.
La différence entre une application amateur et une application professionnelle réside souvent dans sa capacité à gérer l'échec avec grâce. Au lieu de planter, une application robuste doit anticiper les problèmes potentiels et guider l'utilisateur.
Considérons un cas d'usage extrêmement courant : le téléversement d'un fichier CSV. Sans gestion d'erreurs, le code est d'une simplicité trompeuse.
import streamlit as st
import pandas as pd
# ❌ MAUVAIS - Pas de gestion d'erreur
uploaded_file = st.file_uploader("Upload CSV")
if uploaded_file:
df = pd.read_csv(uploaded_file) # 💥 CRASH si fichier invalide
st.write(df)
Que se passe-t-il si l'utilisateur envoie un fichier vide, un fichier JSON, ou un CSV mal formaté ? L'application crashe. Pour l'utilisateur, l'application est simplement "cassée".
La Solution : Le Filet de Sécurité try...except
Le bloc try...except de Python est le principal outil pour construire cette résilience. Il vous permet d'"essayer" une opération risquée et de "capturer" les erreurs si elles se produisent, afin d'exécuter un plan B au lieu de tout arrêter.
Voici comment transformer notre exemple précédent en une expérience utilisateur contrôlée.
import streamlit as st
import pandas as pd
# ✅ BON - Gestion d'erreur propre
uploaded_file = st.file_uploader("Upload CSV")
if uploaded_file:
try:
df = pd.read_csv(uploaded_file)
st.success(f"✅ Fichier chargé avec succès : {len(df)} lignes trouvées.")
st.write(df)
except pd.errors.EmptyDataError:
st.error("❌ Erreur : Le fichier que vous avez envoyé est vide.")
except pd.errors.ParserError:
st.error("❌ Erreur : Impossible de lire le fichier. Veuillez vérifier qu'il s'agit d'un CSV valide.")
except Exception as e:
st.error(f"❌ Une erreur inattendue est survenue : {str(e)}")
Avec cette approche, non seulement l'application ne plante plus, mais elle fournit un retour clair et actionnable à l'utilisateur. Ce même principe est fondamental lors d'interactions avec des services externes, comme les appels d'API, où les sources d'échec sont nombreuses (pas de connexion internet, serveur indisponible, etc.).
Valider en Amont pour Mieux Régner
Une autre facette de la robustesse est la validation proactive des entrées utilisateur. Plutôt que d'attendre qu'une erreur se produise, vous pouvez la prévenir en vérifiant la validité des données dès leur saisie. Si une entrée est invalide, on peut utiliser st.stop() pour interrompre proprement l'exécution du script et éviter des erreurs en cascade.
import streamlit as st
import re
email = st.text_input("Veuillez entrer votre adresse email")
if email:
# On valide le format de l'email avec une expression régulière
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
st.error("❌ Format d'email invalide.")
st.stop() # Arrête l'exécution ici, le code ci-dessous ne sera pas lu
# Le reste du script ne s'exécute que si l'email est valide
st.success(f"✅ Email valide : {email}")
# ... la suite de votre logique ...
Enfin, durant la phase de développement, il est souvent utile de voir l'erreur complète pour la déboguer. st.exception(e) est votre ami : il affiche la "traceback" complète de l'erreur directement dans l'application. En production, vous remplacerez cet appel par un message d'erreur générique pour l'utilisateur, tout en enregistrant l'erreur détaillée dans un système de logs (comme nous le verrons plus tard).
6. L'Optimisation des Images : La Vitesse au Service de l'Expérience Visuelle
Une application de données n'est pas qu'une suite de chiffres et de graphiques. Les éléments visuels, comme les logos, les illustrations ou les photos, jouent un rôle crucial dans l'attractivité et la clarté de votre interface. Cependant, chaque image que vous ajoutez est un poids potentiel qui peut considérablement ralentir votre application.
L'erreur la plus commune est d'utiliser des images brutes, souvent en haute résolution et pesant plusieurs mégaoctets (MB), directement dans l'application.
import streamlit as st
# ❌ MAUVAIS - Une image de 8 MB peut prendre plus de 10 secondes à charger !
st.image("hero_image.png", use_column_width=True)
Le résultat est immédiatement perceptible, surtout sur une connexion mobile. L'utilisateur voit un espace vide qui se charge lentement, ou pire, l'application entière semble figée pendant le téléchargement. Cette attente dégrade l'image professionnelle de votre travail et peut coûter cher en bande passante si l'application est très consultée.
Stratégies pour des Images Légères et Rapides
La gestion des images dans une application web est un art qui balance qualité et performance. Voici une approche multi-niveaux pour y parvenir.
Niveau 1 : L'Optimisation en Amont (La plus efficace)
La meilleure optimisation est celle que vous faites avant même d'écrire la moindre ligne de code. Utilisez des outils, en ligne ou non, pour compresser et redimensionner vos images à une taille raisonnable pour le web. Des services comme TinyPNG ou Squoosh sont excellents pour cela. Pour les utilisateurs plus avancés, des outils en ligne de commande comme ImageMagick permettent d'automatiser ce processus.
# Exemple avec ImageMagick
# Convertir en WebP, un format moderne offrant une excellente compression
convert image.png -quality 80 image.webp
# Simplement redimensionner une image à une largeur de 1200 pixels
convert image.png -resize 1200x image_optimized.png
Niveau 2 : L'Optimisation Programmatique comme Garde-Fou
Pour ajouter une couche de sécurité, vous pouvez également traiter les images directement dans votre code Python avec la librairie Pillow (PIL). Cette approche est utile pour garantir que toutes les images respectent certaines contraintes, par exemple si elles sont téléversées par des utilisateurs ou gérées par plusieurs développeurs. En combinant cette technique avec le caching, vous créez un système robuste où l'image est optimisée une seule fois.
import streamlit as st
from PIL import Image
@st.cache_data
def load_and_optimize_image(image_path):
"""Charge une image et la redimensionne si elle est trop grande."""
img = Image.open(image_path)
# On definit une taille maximale (largeur, hauteur)
max_size = (1200, 1200)
# `thumbnail` redimensionne l'image en conservant ses proportions
img.thumbnail(max_size, Image.Resampling.LANCZOS)
return img
# ✅ BON - L'image est optimisée et le résultat mis en cache
img = load_and_optimize_image("hero_image.png")
st.image(img, use_column_width=True)
En règle générale, visez des tailles de fichier qui ne pénalisent pas l'utilisateur. Un fichier PNG (pour les images avec transparence) ne devrait idéalement pas dépasser 500 KB. Pour un JPEG ou un WebP (pour toutes les autres images), une cible sous les 200 KB est excellente. Enfin, à moins d'afficher des photographies en plein écran, une largeur de 1200 à 1500 pixels est largement suffisante pour la plupart des écrans.
7. La Stabilité de l'État : L'Importance des key dans les Widgets
À mesure qu'une application grandit, il est courant d'afficher plusieurs widgets du même type sur une page. Imaginez un tableau de bord avec deux st.slider côte à côte. Comment Streamlit fait-il la différence entre les deux lors d'un rerun pour leur réaffecter la bonne valeur ?
Par défaut, Streamlit se base sur l'ordre d'apparition des widgets dans le code. C'est une stratégie qui fonctionne pour les scripts très simples, mais qui devient extrêmement fragile dès que la structure de l'application se complexifie (par exemple, si des widgets sont affichés ou masqués de manière conditionnelle).
L'exemple suivant, bien que simple, illustre ce risque. Deux sliders sont créés, sans identifiant unique.
import streamlit as st
# ❌ MAUVAIS - Widgets identiques sans key
col1, col2 = st.columns(2)
with col1:
value1 = st.slider("Valeur 1", 0, 100) # Pas de key !
with col2:
value2 = st.slider("Valeur 2", 0, 100) # Pas de key !
# Les valeurs peuvent se mélanger lors des reruns ! 😱
Dans ce scénario, un changement dans la logique d'affichage pourrait amener Streamlit à inverser les états des deux sliders, créant des bugs d'état incohérents et très difficiles à déboguer. L'utilisateur déplace un curseur, et c'est la valeur d'un autre qui change.
La Solution : Donner une Identité Unique avec key
Pour fiabiliser la gestion de l'état, il faut donner une identité explicite et stable à chaque widget. C'est le rôle du paramètre key. En assignant une key unique à un widget, vous créez un lien permanent entre ce widget et sa valeur dans st.session_state.
import streamlit as st
# ✅ BON - Keys uniques et stables
col1, col2 = st.columns(2)
with col1:
value1 = st.slider("Valeur 1", 0, 100, key="slider_1")
with col2:
value2 = st.slider("Valeur 2", 0, 100, key="slider_2")
Avec cette modification, l'état de chaque slider est maintenant géré de manière fiable, peu importe les changements dans le reste de l'application. La key devient l'ancre qui stabilise l'état.
Cette pratique est absolument indispensable lorsque vous générez des widgets de manière dynamique, par exemple dans une boucle. Il faut alors construire une clé unique pour chaque itération.
import streamlit as st
# Générer une liste de questions
for i in range(5):
# ✅ La clé est rendue unique grâce à l'index de la boucle
st.text_input(
f"Question {i+1}",
key=f"question_{i}"
)
L'utilisation d'une key a un autre avantage majeur : la valeur du widget est automatiquement synchronisée avec st.session_state. Vous pouvez donc accéder à la valeur de n'importe quel widget, depuis n'importe où dans votre code, simplement en consultant st.session_state.ma_cle. Cela renforce le rôle de st.session_state comme source unique de vérité pour l'état de votre application.
import streamlit as st
st.slider("Âge de l'utilisateur", 0, 100, key="user_age")
# On peut maintenant accéder à cette valeur ailleurs dans le code
age = st.session_state.user_age
st.write(f"L'âge enregistré est de {age} ans.")
Prendre l'habitude de nommer systématiquement vos widgets avec des clés descriptives (par exemple, {type_widget}_{contexte}_{id}) est une discipline qui portera ses fruits en rendant vos applications plus robustes, plus lisibles et plus faciles à maintenir.
8. La Sécurité du Markdown : Se Protéger Contre les Attaques XSS
La commande st.markdown() est l'un des outils les plus polyvalents de Streamlit, permettant de formater du texte, d'intégrer des listes, des tableaux, et bien plus. Pour des besoins de design avancés, il peut être tentant d'utiliser son paramètre le plus puissant : unsafe_allow_html=True. Ce drapeau vous donne un contrôle total sur le rendu en vous autorisant à injecter du HTML et du CSS brut.
Cependant, ce pouvoir implique une grande responsabilité. Le mot "unsafe" (non sécurisé) est un avertissement clair : si vous combinez cette option avec du contenu provenant d'un utilisateur, vous ouvrez une porte béante aux attaques de type Cross-Site Scripting (XSS).
Imaginez que vous affichiez un commentaire laissé par un utilisateur. Si ce dernier est malveillant, il peut injecter une balise <script> dans son message.
import streamlit as st
# ❌ DANGEREUX - Injection de code possible !
user_input = st.text_input("Laissez un commentaire")
if user_input:
# Si l'utilisateur entre "<script>alert('XSS Attack!')</script>",
# ce script sera exécuté par le navigateur de la prochaine personne qui visite la page.
st.markdown(user_input, unsafe_allow_html=True)
L'exemple ci-dessus est bénin, mais un attaquant pourrait utiliser cette faille pour voler les cookies de session d'autres utilisateurs, les rediriger vers un site de phishing, ou défigurer votre application. C'est une vulnérabilité de sécurité majeure.
La Règle d'Or : Ne Jamais Faire Confiance au Contenu Utilisateur
La règle est simple et absolue : n'utilisez jamais unsafe_allow_html=True avec une chaîne de caractères qui provient, de près ou de loin, d'une entrée utilisateur. Par défaut, Streamlit vous protège en "échappant" tout le HTML. Les balises sont simplement affichées comme du texte, neutralisant ainsi toute menace.
import streamlit as st
# ✅ BON - HTML échappé par défaut
user_input = st.text_input("Laissez un commentaire")
if user_input:
st.markdown(user_input) # Sécurisé, les balises <script> seront affichées comme du texte.
Solution Avancée : Autoriser le HTML en Toute Sécurité avec la "Sanitization"
Que faire si vous souhaitez tout de même autoriser vos utilisateurs à formater leur texte (mettre en gras, en italique, etc.) ? La solution professionnelle est de "nettoyer" (ou "sanitizer") leur entrée. Cela consiste à n'autoriser qu'une liste blanche de balises HTML sûres et à supprimer toutes les autres.
La bibliothèque Python bleach est l'outil standard pour cette tâche.
import streamlit as st
import bleach
# On définit une liste blanche de balises HTML que l'on considère comme sûres.
ALLOWED_TAGS = ["b", "i", "u", "strong", "em", "p", "br"]
user_input = st.text_area("Votre commentaire (gras, italique et souligné sont autorisés)")
if user_input:
# ✅ On nettoie le HTML fourni par l'utilisateur
clean_html = bleach.clean(
user_input,
tags=ALLOWED_TAGS,
strip=True # Supprime les balises non autorisées au lieu de les échapper
)
# On peut maintenant l'afficher en toute sécurité
st.markdown(clean_html, unsafe_allow_html=True)
Cette approche vous offre le meilleur des deux mondes : une flexibilité pour vos utilisateurs et une sécurité robuste pour votre application. Pensez à ajouter bleach à vos dépendances (pip install bleach).
9. La Gestion Efficace des Connexions à la Base de Données
Établir une connexion à une base de données est une opération coûteuse. Elle consomme du temps et des ressources, à la fois pour votre application et pour le serveur de la base de données. Dans le contexte du modèle de rerun de Streamlit, la manière dont vous gérez ces connexions a un impact direct sur la capacité de votre application à monter en charge.
L'approche la plus naïve, et malheureusement la plus courante, consiste à créer une nouvelle connexion à chaque fois que l'on doit récupérer des données.
import streamlit as st
import psycopg2
# ❌ MAUVAIS - Une nouvelle connexion est créée à chaque interaction de l'utilisateur !
def get_data():
conn = psycopg2.connect(**st.secrets["db"])
cur = conn.cursor()
cur.execute("SELECT * FROM users")
results = cur.fetchall()
cur.close()
conn.close()
return results
# À chaque rerun, on ouvre et on ferme une connexion.
data = get_data()
st.write(data)
Cette méthode fonctionne pour un seul utilisateur en développement local. Mais en production, c'est une recette pour le désastre. Chaque utilisateur, chaque interaction, ouvre une nouvelle connexion. La base de données se retrouve rapidement submergée et commence à rejeter les nouvelles demandes, provoquant des erreurs "too many connections" et le crash de votre application.
Solution 1 : La Connexion Unique avec @st.cache_resource
Nous avons déjà abordé la solution idiomatique de Streamlit pour gérer les "ressources" partagées : le décorateur @st.cache_resource. En l'appliquant à votre fonction de connexion, vous la transformez en un singleton. Cela signifie que la connexion est établie une seule fois et que ce même objet de connexion est ensuite réutilisé pour tous les reruns et par tous les utilisateurs.
import streamlit as st
import psycopg2
# ✅ BON - La connexion est créée une fois et réutilisée.
@st.cache_resource
def get_connection():
return psycopg2.connect(**st.secrets["db"])
def get_data():
conn = get_connection() # Réutilise la connexion existante
cur = conn.cursor()
cur.execute("SELECT * FROM users")
results = cur.fetchall()
cur.close()
return results
data = get_data()
st.write(data)
Cette simple modification résout le problème de saturation et représente le strict minimum pour une application en production.
Solution 2 : Le Pooling de Connexions pour la Montée en Charge
Pour les applications à fort trafic, on peut aller encore plus loin avec une technique standard dans le monde du développement web : le pooling de connexions. Au lieu de n'avoir qu'une seule connexion partagée, on crée un "pool" (une réserve) de plusieurs connexions que l'application peut "emprunter" et "rendre" au besoin.
Cela améliore la performance et la stabilité sous forte charge. La bibliothèque SQLAlchemy est l'outil de choix pour mettre en place un pooling de connexions robuste en Python.
import streamlit as st
from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool
@st.cache_resource
def get_engine():
"""Crée un "engine" SQLAlchemy avec un pool de connexions."""
return create_engine(
f"postgresql://{st.secrets['db']['user']}:{st.secrets['db']['password']}@{st.secrets['db']['host']}/{st.secrets['db']['database']}",
poolclass=QueuePool,
pool_size=5, # Le pool maintiendra 5 connexions ouvertes
max_overflow=10, # Peut créer 10 connexions supplémentaires si besoin
pool_pre_ping=True # Vérifie que la connexion est toujours active avant de l'utiliser
)
def get_data():
engine = get_engine()
with engine.connect() as conn:
result = conn.execute("SELECT * FROM users")
return result.fetchall()
data = get_data()
st.write(data)
Cette approche, bien que légèrement plus complexe, est la marque d'une application conçue pour être scalable et résiliente. Elle démontre une compréhension approfondie des enjeux de performance au-delà du simple code de l'application.
10. Le Pilotage par la Donnée : Monitorer son Application en Production
Déployer une application sans monitoring, c'est comme piloter un avion les yeux bandés. Vous espérez être arrivé à destination, mais vous n'avez aucune information sur votre altitude, votre vitesse, ou les alarmes qui clignotent peut-être dans le cockpit. En production, l'absence de monitoring vous laisse dans le noir : vous ne savez pas si votre application est lente, si elle plante, ni même si elle est utilisée.
Les bugs ne sont alors plus découverts par des outils, mais signalés (parfois) par des utilisateurs frustrés. Et même dans ce cas, vous manquez de contexte pour reproduire et corriger le problème efficacement. C'est le signe d'une approche réactive, et non proactive, de la maintenance.
La Solution : Intégrer des Outils de Monitoring
Un développeur professionnel ne se contente pas de livrer une application ; il s'assure qu'elle fonctionne de manière fiable. Pour cela, l'intégration d'un outil de suivi d'erreurs et de performance est indispensable.
La voie royale : les services de monitoring
Des outils comme Sentry sont devenus un standard de l'industrie. En quelques lignes de code, ils s'intègrent à votre application et vous fournissent une tour de contrôle complète. Chaque fois qu'une erreur non capturée se produit, Sentry l'enregistre et l'enrichit avec un contexte précieux (version du navigateur, système d'exploitation, actions de l'utilisateur précédant l'erreur), vous permettant de diagnostiquer et de résoudre les bugs à une vitesse fulgurante.
import streamlit as st
import sentry_sdk
# Initialisation de Sentry au tout début du script
sentry_sdk.init(
dsn=st.secrets["sentry"]["dsn"], # Votre clé DSN stockée dans les secrets
traces_sample_rate=1.0, # Capture 100% des transactions pour l'analyse de perf
environment="production"
)
# Le reste de votre application...
st.title("Mon Dashboard Professionnel")
try:
# Une opération qui pourrait échouer
result = 1 / 0
except Exception as e:
# L'erreur est automatiquement envoyée à Sentry
st.error("Oups, une erreur est survenue. L'équipe technique a été prévenue.")
# On peut aussi l'envoyer manuellement pour plus de contrôle
sentry_sdk.capture_exception(e)
Au-delà de la simple capture d'erreurs, ces outils vous permettent de suivre des événements personnalisés ou de contextualiser les problèmes, par exemple en associant une erreur à l'email de l'utilisateur concerné pour pouvoir le recontacter.
L'alternative pragmatique : le logging
Si vous n'êtes pas prêt à intégrer un service tiers, la mise en place d'un système de logs est un excellent premier pas. Le module logging de Python, configuré pour écrire dans un fichier, vous donnera une trace écrite des événements importants, des erreurs et des temps de chargement. C'est une source d'information précieuse pour le débogage a posteriori.
import logging
# Configuration simple pour écrire dans un fichier app.log
logging.basicConfig(
filename='app.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# Exemples de logs
logging.info(f"Utilisateur {user_email} a démarré une session.")
logging.error(f"Le chargement des données a échoué : {e}")
Le monitoring n'est pas un luxe, c'est une nécessité. C'est le seul moyen de savoir objectivement si votre application remplit sa mission et d'offrir un service de qualité à vos utilisateurs.
Votre Check-list Avant le Décollage
Avant de rendre votre application publique, prenez le temps de passer en revue cette check-list. Elle synthétise les points que nous avons vus et constitue votre dernière vérification avant le déploiement.
✅ Performance
- Les opérations coûteuses (chargement de données, calculs) sont mises en cache avec
@st.cache_dataou@st.cache_resource. - Les images sont compressées et redimensionnées pour le web (< 500 KB).
- Le script principal est épuré de tout calcul lourd non mis en cache.
- Les formulaires (
st.form) sont utilisés pour regrouper les widgets et limiter les reruns.
✅ Sécurité
- Aucune clé d'API ou mot de passe n'est écrit en dur dans le code.
- Le fichier
secrets.tomlest bien présent dans le.gitignore. -
unsafe_allow_html=Truen'est jamais utilisé avec des données provenant de l'utilisateur. - Les entrées utilisateur sont validées avant d'être traitées.
✅ Robustesse
- Les opérations à risque (lectures de fichiers, appels API) sont encapsulées dans des blocs
try...except. - Des messages d'erreur clairs et utiles sont affichés à l'utilisateur.
- Toutes les variables du
session_statesont systématiquement initialisées. - Chaque widget interactif possède une
keyunique et descriptive.
✅ Infrastructure
- La connexion à la base de données est gérée via
@st.cache_resource(ou mieux, un pool de connexions). - Un système de monitoring d'erreurs (Sentry) ou de logging est en place.
Conclusion : De l'Artisanat à l'Ingénierie
Nous avons parcouru ensemble les dix erreurs qui séparent le plus souvent un prototype fonctionnel d'une application de production fiable et performante. Du caching à la gestion des erreurs, en passant par la sécurité et le monitoring, chaque point est une brique essentielle dans la construction d'un produit de qualité.
La simplicité de Streamlit est une invitation à créer, mais sa maîtrise réside dans la compréhension de ces détails d'implémentation. En internalisant ces principes, vous ne vous contentez plus de faire fonctionner vos applications ; vous les concevez pour être robustes, scalables et dignes de confiance.
La différence entre un artisan et un ingénieur n'est pas la complexité des outils, mais la rigueur du processus. Avec ce guide, vous avez désormais les clés pour appliquer une démarche d'ingénierie à vos projets Streamlit et pour construire des applications qui auront un impact durable.
Pour aller plus loin
Si vous voulez maîtriser Streamlit de A à Z et éviter tous les pièges, j'ai créé une formation complète :
👉 Streamlit Unleashed - 20h de contenu, 50+ exercices pratiques, section dédiée aux best practices et optimisations
Outils gratuits
- Roast My Streamlit : Audit gratuit de votre app Streamlit (détecte les erreurs de performance et sécurité)
- Streamlit Cheatsheet : Aide-mémoire PDF gratuit avec toutes les bonnes pratiques
Articles complémentaires
- Dashboard interactif avec Streamlit et Plotly
- Streamlit Authentication : Guide complet 2025
- Déployer une app Streamlit sur Streamlit Cloud
Vous avez d'autres erreurs courantes à partager ? Contactez-moi sur LinkedIn ou par email à gael@mes-formations-data.fr.
Cet article est basé sur mon expérience de 50+ applications Streamlit créées pour des clients en production. Chaque erreur listée m'a coûté des heures de debugging... pour que vous n'ayez pas à les refaire !

📚 Approfondir avec mon livre
"Business Intelligence avec Python" - Le guide complet pour maîtriser l'analyse de données
Voir sur Amazon →