cours1 min de lecture

Mise au point des programmes

Anticiper les bugs typiques, durcir le code par des assertions, des doctests et des jeux de tests automatisés.
programme

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 :

FamilleSymptômeExemple
TypageMélange d'int, float, str."3" + 4 lève TypeError.
Effets de bordUne fonction modifie ses arguments mutables.def f(l): l.append(0)
Hors-bornesIndice de tableau ou borne range mal placée.range(5) produit 0..4.
Conditionnelle non-exhaustiveUn cas oublié dans une cascade if/elif.elif sans else final.
FlottantsComparaison d'égalité entre float.0.1 + 0.2 == 0.3 est faux.
La comparaison d'égalité entre flottants est l'un des pièges les plus classiques. Préférez abs(a - b) < epsilon pour un epsilon adapté à votre précision attendue.
⏵ Ctrl+↵ pour exécuter
Aucune exécution pour l'instant.

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.
Une assertion documente le contrat et le vérifie. Quand un nouvel utilisateur appelle votre fonction avec un mauvais argument, l'erreur indique exactement quelle hypothèse a été violée.

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 sur n : range(5) produit 0, 1, 2, 3, 4.
  • Indices Python à partir de 0 : t[len(t)] est toujours hors-bornes.
  • Comparaison de chaînes vs nombres : "10" < "9" vaut True (ordre lexicographique).
  • Argument mutable par défaut : def f(l=[]) partage la même liste entre tous les appels. Le pattern canonique est def f(l=None) puis, dans le corps, if l is None: l = []. (À éviter : l'idiome raccourci l = 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 quand l doit être muté en place.)
  • elif sans else : 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.