Mise au point des programmes
Introduction
Un programme qui « tourne » n'est pas un programme qui fonctionne. Entre les deux, il y a tout un travail d'ingénierie défensive : anticiper les cas limites, expliciter les hypothèses, instrumenter le code pour que la moindre incohérence se signale d'elle-même. Ce travail est l'objet de l'item .
Une image utile : du code sans tests, c'est un numéro de trapèze sans filet. Tant que rien ne dérape, tout va bien — au premier faux pas, plus rien ne rattrape. Tests et assertions servent précisément à tendre ce filet.
Vous connaissez déjà la spécification d'une fonction (chapitre PG) et l'usage ponctuel des assertions. En Terminale, on systématise : chaque fonction exposée doit avoir un contrat vérifiable, un jeu de tests qui exerce ses cas limites, et le développeur doit savoir reconnaître les familles de bugs récurrentes avant qu'elles n'apparaissent en production.
Les bugs typiques à reconnaître
Cinq familles reviennent en boucle :
| Famille | Symptôme | Exemple |
|---|---|---|
| Typage | Mélange d'int, float, str. | "3" + 4 lève TypeError. |
| Effets de bord | Une fonction modifie ses arguments mutables. | def f(l): l.append(0) |
| Hors-bornes | Indice de tableau ou borne range mal placée. | range(5) produit 0..4. |
| Conditionnelle non-exhaustive | Un cas oublié dans une cascade if/elif. | elif sans else final. |
| Flottants | Comparaison d'égalité entre float. | 0.1 + 0.2 == 0.3 est faux. |
abs(a - b) < epsilon pour un epsilon adapté à votre
précision attendue.Quelle expression renvoie True ?
Les assertions — durcir une fonction
Une assertion vérifie qu'une condition est vraie à un endroit donné du
programme. Si elle est fausse, Python lève une AssertionError immédiatement —
le bug est attrapé au plus tôt, à l'endroit même où la violation a eu lieu.
def division_entiere(a: int, b: int) -> int:
"""Retourne le quotient entier de a par b."""
assert isinstance(a, int), "a doit être un entier"
assert isinstance(b, int), "b doit être un entier"
assert b != 0, "division par zéro interdite"
return a // b
Les assertions se placent typiquement :
- en entrée de fonction pour valider les pré-conditions (typage, plages de valeurs) ;
- en sortie pour vérifier une post-condition (invariant attendu) ;
- dans une boucle pour matérialiser un invariant.
Doctests et pytest — un jeu de tests automatisé
Vérifier à la main qu'une fonction marche après chaque modification est inefficace. On automatise.
Doctests — tests dans la docstring
def double(n: int) -> int:
"""Retourne le double de n.
>>> double(3)
6
>>> double(-2)
-4
>>> double(0)
0
"""
return 2 * n
Exécution : python -m doctest mon_fichier.py -v. Chaque ligne >>> est
exécutée et le résultat est comparé à la ligne suivante. Léger et intégré à
la documentation.
pytest — la solution industrielle
# fichier test_calculs.py
from calculs import division_entiere
def test_division_simple():
assert division_entiere(10, 3) == 3
def test_division_negative():
assert division_entiere(-7, 2) == -4 # division Python arrondit vers -inf
def test_division_par_zero():
import pytest
with pytest.raises(AssertionError):
division_entiere(10, 0)
Exécution : pytest. Le framework découvre tous les fichiers test_*.py,
exécute toutes les fonctions test_*, et affiche un rapport.
Que vérifie le test test_division_par_zero ?
Pièges récurrents à connaître
range(n)est exclusif surn:range(5)produit0, 1, 2, 3, 4.- Indices Python à partir de 0 :
t[len(t)]est toujours hors-bornes. - Comparaison de chaînes vs nombres :
"10" < "9"vautTrue(ordre lexicographique). - Argument mutable par défaut :
def f(l=[])partage la même liste entre tous les appels. Le pattern canonique estdef f(l=None)puis, dans le corps,if l is None: l = []. (À éviter : l'idiome raccourcil = l or []traite toute valeur falsy — y compris une liste vide passée volontairement par l'appelant — comme un cas « non fourni » et la remplace par une nouvelle liste. C'est rarement ce qu'on veut quandldoit être muté en place.) elifsanselse: un cas oublié = comportement silencieux indéfini.
Pour aller plus loin
Le module standard logging permet d'instrumenter un programme sans recourir
à des print qu'on devra supprimer. Le typage est renforcé statiquement par
mypy, qui détecte les erreurs de type avant l'exécution. Et pytest
gère paramétrisation, fixtures et couverture de code.