Tester du code pandas avec pytest : la règle des 3 tests pour data analysts pressés
Un test pandas utile ne vérifie pas que pandas fonctionne. Il vérifie qu'une règle métier reste vraie : que le CA se recalcule bien depuis les quantités, que les doublons disparaissent, que les lignes à quantité négative sont écartées. Trois tests de ce type sécurisent plus qu'une suite de trente tests techniques.
Voici la méthode, avec le code exact.
Pourquoi les tests data échouent à s'installer dans les équipes
L'objection la plus fréquente n'est pas « je ne sais pas écrire un test », c'est « je livre déjà en retard ». Elle est fondée : sur un pipeline data, écrire des tests exhaustifs coûte plus cher que le pipeline lui-même.
La sortie de ce piège n'est pas de tout tester. C'est de tester ce qui casse silencieusement.
| Type de bug | Détecté sans test ? | Coût de détection tardive |
|---|---|---|
FileNotFoundError |
Oui, immédiatement | Faible |
| Erreur de syntaxe | Oui, immédiatement | Nul |
| Jointure qui duplique les lignes | Non | CA gonflé, découvert en comité |
| Filtre inversé | Non | Rapport faux pendant 3 mois |
| Colonne renommée à la source | Non | Colonne remplie de NaN |
Les trois lignes en gras sont votre cible. Elles ont un point commun : le script ne plante pas, il produit un résultat faux.
Le squelette pytest minimum
projet/
├── src/ventes/transform.py
└── tests/
├── conftest.py
└── test_transform.py
uv add --dev pytest
uv run pytest
pytest découvre seul les fichiers test_*.py et les fonctions test_*. Aucune configuration n'est nécessaire pour démarrer.
Test 1 : la règle métier centrale
Chaque pipeline data a une règle qui, si elle casse, invalide tout le reste. Sur un pipeline de ventes, c'est le calcul du chiffre d'affaires.
# tests/test_transform.py
import pandas as pd
from ventes.transform import calculer_ca
def test_ca_est_recalcule_depuis_les_quantites():
"""Le CA fourni par la source n'est jamais fiable : on recalcule toujours."""
df = pd.DataFrame({
"quantite": [2, 5],
"prix_unitaire": [10.0, 3.0],
"ca": [999.0, 999.0], # valeurs pourries volontairement
})
resultat = calculer_ca(df)
assert resultat["ca"].tolist() == [20.0, 15.0]
Ce test fait deux choses en cinq lignes : il vérifie le calcul, et il documente une décision (on ignore la colonne ca de la source). Le prochain développeur qui voudra « optimiser » en réutilisant la colonne fournie verra le test rouge et comprendra pourquoi.
Test 2 : la volumétrie après jointure
La jointure qui duplique est le bug data le plus coûteux, parce qu'il gonfle les agrégats sans jamais lever d'exception.
def test_jointure_ne_duplique_pas_les_lignes():
ventes = pd.DataFrame({"produit_id": [1, 2, 3], "quantite": [5, 5, 5]})
referentiel = pd.DataFrame({"produit_id": [1, 2, 3], "categorie": ["A", "B", "C"]})
resultat = enrichir_produits(ventes, referentiel)
assert len(resultat) == len(ventes)
assert resultat["categorie"].notna().all()
Deux assertions, deux risques couverts : la duplication (jointure many-to-many involontaire) et la perte (clé absente du référentiel qui produit des NaN).
En production, la même logique se met directement dans le code :
def enrichir_produits(ventes: pd.DataFrame, referentiel: pd.DataFrame) -> pd.DataFrame:
resultat = ventes.merge(referentiel, on="produit_id", how="left", validate="many_to_one")
return resultat
validate="many_to_one" fait échouer le merge si le référentiel contient des doublons de clé. C'est un test qui tourne à chaque exécution, pour zéro ligne de test.
Test 3 : les données de bord
Le troisième test porte sur ce qui arrive rarement mais casse tout : DataFrame vide, valeurs manquantes, types inattendus.
import pytest
def test_pipeline_sur_dataframe_vide():
"""Un fichier source vide ne doit pas planter, mais produire un resultat vide."""
df = pd.DataFrame(columns=["quantite", "prix_unitaire"])
resultat = calculer_ca(df)
assert resultat.empty
def test_colonne_manquante_leve_une_erreur_explicite():
df = pd.DataFrame({"quantite": [1]})
with pytest.raises(KeyError, match="prix_unitaire"):
calculer_ca(df)
Le second test est contre-intuitif : on vérifie que le code plante bien. C'est volontaire. Un pipeline qui échoue bruyamment à 6 h du matin coûte moins cher qu'un pipeline qui produit un rapport faux.
Comparer deux DataFrames sans se faire piéger
df1 == df2 ne fait pas ce que vous croyez : il compare élément par élément et renvoie un DataFrame de booléens, pas un verdict.
from pandas.testing import assert_frame_equal
def test_nettoyage_produit_le_resultat_attendu():
entree = pd.DataFrame({"region": ["Nord ", "sud", "NORD"], "quantite": [1, 2, 3]})
attendu = pd.DataFrame({"region": ["nord", "sud", "nord"], "quantite": [1, 2, 3]})
assert_frame_equal(normaliser_regions(entree), attendu)
assert_frame_equal vérifie valeurs, types et index, et produit un message de diff lisible en cas d'échec. Options utiles :
| Option | Usage |
|---|---|
check_dtype=False |
Quand int64 vs int32 vous importe peu |
check_like=True |
Ignore l'ordre des colonnes et de l'index |
atol=1e-6 |
Tolérance sur les flottants, indispensable sur des ratios |
Le piège classique : après un df[df["x"] > 0], l'index n'est plus 0,1,2. Ajoutez .reset_index(drop=True) dans la fonction testée, ou comparez avec check_like=True.
Les fixtures, pour arrêter de recopier le même DataFrame
# tests/conftest.py
import pandas as pd
import pytest
@pytest.fixture
def ventes_minimales() -> pd.DataFrame:
return pd.DataFrame({
"date": pd.to_datetime(["2026-01-01", "2026-01-02"]),
"region": ["nord", "sud"],
"produit": ["A", "B"],
"quantite": [2, 5],
"prix_unitaire": [10.0, 3.0],
})
Toute fonction de test qui déclare un paramètre ventes_minimales le reçoit automatiquement, reconstruit à neuf à chaque test. Placée dans conftest.py, la fixture est disponible dans tous les fichiers de tests sans import.
Règle de dimensionnement : une fixture de test fait 2 à 4 lignes de données, pas 10 000. Si votre test a besoin d'un vrai fichier de 500 Mo, ce n'est plus un test unitaire, c'est un test d'intégration, et il ne tourne pas à chaque commit.
Faire tourner ça automatiquement
Un test qu'on oublie de lancer ne sert à rien. Deux niveaux d'automatisation, par ordre de coût :
# .github/workflows/tests.yml
name: tests
on: [push, pull_request]
jobs:
pytest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv sync
- run: uv run pytest
Onze lignes, et la pastille rouge apparaît sur chaque pull request qui casse une règle métier. C'est le meilleur rapport effort / valeur de toute cette liste.
« Personne ne relit mon code de toute façon »
C'est souvent vrai, et c'est justement l'argument pour les tests plutôt que contre. Sans relecteur, le test est la seule chose qui vous dira que vous venez de casser le calcul du CA en refactorant un filtre.
Le point de bascule mesurable : le jour où vous modifiez un pipeline sans avoir peur, parce que trois tests répondent en 2 secondes.
Le cas devient encore plus net quand une partie du code est générée par IA : le test est ce qui arbitre à votre place, comme détaillé dans les 7 vérifications sur le code data généré par l'IA. Pour situer votre pratique actuelle, le quiz code production-ready donne un score en 15 questions. Le découpage en fonctions qui rend le code testable est détaillé dans passer du notebook au script Python, et l'emplacement de tests/ dans structurer un projet Python data.
L'ensemble tests, CI et revue de code sur un projet data complet est traité dans DataCraft.
Questions fréquentes
Combien de tests pour un pipeline data ? Un par règle métier non évidente. En pratique, 5 à 15 tests couvrent un pipeline de production, à condition qu'ils portent sur les règles et non sur la plomberie.
Faut-il tester le chargement de fichiers ? Non, pas en test unitaire. Testez la transformation, qui contient la logique. Le chargement se vérifie par une validation de schéma à l'exécution (pandera, ou un simple contrôle des colonnes attendues).
Comment tester du code qui interroge une base de données ? Séparez la requête du traitement. La fonction qui interroge renvoie un DataFrame, la fonction qui calcule prend un DataFrame. Seule la seconde a besoin de tests.

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