Séance 08 · Partie A · Python pas à pas
Erreurs, tests
et débogage.
Un programme qui marche du premier coup est un programme qu’on n’a pas assez testé. Savoir lire une erreur, la provoquer, la prévoir et la traquer : c’est la moitié du métier.
Durée : 90 min · Objectifs : lire une trace d’erreur, distinguer les familles d’erreurs, try / except, écrire des tests, adopter une méthode de débogage.
Ce que vous saurez faire à la fin
- Trouver la ligne fautive et le type d’erreur en trois secondes.
- Rendre un programme robuste face aux saisies et fichiers invalides.
- Écrire des tests qui vous préviennent avant l’utilisateur.
- Déboguer méthodiquement, sans « essayer au hasard ».
01 / Lire
Anatomie d’un message d’erreur
La trace (traceback) se lit de bas en haut :
- Dernière ligne : le type et le message.
ZeroDivisionError: division by zero. - Juste au-dessus : la ligne exacte où ça a cassé (ligne 2, dans
moyenne). - Plus haut : la chaîne des appels qui y a mené (
rapportappelée ligne 9).
L’erreur est ligne 2, mais la cause est ligne 9 : une liste vide. La trace vous mène de l’une à l’autre.
01 / Lire
Les trois familles
| Famille | Quand | Exemples |
|---|---|---|
| Syntaxe | Avant l’exécution : Python ne comprend pas le texte | SyntaxError (parenthèse, deux-points oubliés), IndentationError |
| Exécution | Pendant : une opération impossible | NameError, TypeError, ValueError, IndexError, KeyError, ZeroDivisionError, FileNotFoundError |
| Logique | Jamais signalée : le programme tourne mais donne un résultat faux | Une borne de boucle décalée, un signe inversé, une unité oubliée |
La plus dangereuse
La troisième. Python ne peut pas savoir que vous vouliez <= et non <. La sonde Mars Climate Orbiter a été perdue en 1999 parce qu’un logiciel calculait en livres-force et l’autre en newtons : aucun message d’erreur, 300 millions de dollars. Seuls des tests détectent les erreurs de logique.
01 / Lire
Reconnaître au premier coup d’œil
Réflexe
Copier le type d’erreur et son message dans un moteur de recherche est un réflexe d’ingénieur, pas une honte. Mais avant : relisez la ligne indiquée. Neuf fois sur dix, l’erreur est visible une fois qu’on sait quel type chercher.
02 / Gérer
try / except : prévoir l’imprévu
- Le bloc
tryest exécuté normalement. - Si une erreur du type indiqué survient, on saute au bloc
exceptau lieu de planter. - Si aucune erreur :
exceptest ignoré.
Tapez « 50 », puis « vite ». Puis remplacez ValueError par TypeError et retapez « vite ».
Pourquoi préciser le type ?
Un except: nu attrape tout, y compris les erreurs que vous n’aviez pas prévues, y compris une faute de frappe dans votre propre code : le programme continue silencieusement avec un état incohérent. C’est pire qu’un plantage. Attrapez ce que vous savez gérer, laissez passer le reste.
02 / Gérer
Redemander jusqu’à obtenir une valeur valide
Comparaison avec la séance 04
On avait écrit la saisie robuste avec isdigit(). Cette version accepte aussi les négatifs (« -5 » n’est pas digit mais est un entier) et sépare clairement « pas un entier » de « hors bornes ». Le return dans la boucle infinie est la sortie normale. Cette fonction se réutilise partout : c’est la brique « saisie fiable ».
02 / Gérer
Plusieurs erreurs, else, finally, et lever soi-même
raise : signaler une erreur
Une fonction qui reçoit une valeur absurde ne doit pas « faire de son mieux » : elle doit refuser, avec raise et un message clair. C’est l’appelant qui décide quoi faire. as e récupère l’objet erreur pour lire son message. On peut définir ses propres types d’erreur (séance 16). finally sert à libérer une ressource (fermer un port série, couper un moteur) quoi qu’il arrive : essentiel en robotique.
03 / Tester
Les tests : des erreurs que vous provoquez vous-même
Quels cas tester ?
Le cas normal, les bords (0, 360, liste vide, un seul élément), les valeurs négatives, les flottants avec tolérance, et le cas qui doit échouer. Une fonction avec ses tests est une fonction dont on peut changer l’implémentation sans peur : relancez les tests, s’ils passent c’est bon. Dans un vrai projet on utilise le module pytest qui trouve et exécute automatiquement toutes les fonctions test_….
03 / Tester
Tester qu’une erreur se produit bien
Le message d’assert
assert condition, "message" affiche le message si la condition est fausse. Le motif try / except / else / assert False vérifie qu’une erreur a bien été levée : refuser les mauvaises valeurs fait partie du contrat de la fonction, donc ça se teste.
04 / Déboguer
La méthode, quand le programme tourne mais se trompe
- Reproduire
Trouver l’entrée la plus petite qui déclenche le problème. - Localiser
Desprintbien placés (ou un débogueur) pour voir les valeurs à chaque étape. Couper le programme en deux : le bug est-il avant ou après ? - Formuler une hypothèse
« La boucle s’arrête un tour trop tôt parce que… ». Une seule à la fois. - Vérifier
Changer une seule chose, relancer. Si ça ne corrige pas, revenir en arrière. - Protéger
Ajouter un test qui aurait détecté ce bug. Il ne reviendra pas.
Changer trois choses à la fois « pour voir » est la façon la plus sûre de créer deux nouveaux bugs.
04 / Déboguer
Exercice de débogage : trouvez les trois bugs
Indices puis correction
Indice 1 : affichez debut et fenetre à chaque tour. Indice 2 : que vaut valeurs[-2:1] ? Indice 3 : au début, la fenêtre a moins de taille éléments.
def moyenne_mobile(valeurs, taille):
resultat = []
for i in range(len(valeurs)):
debut = max(0, i - taille + 1) # bug 1 : +1 ; bug 2 : jamais négatif
fenetre = valeurs[debut:i + 1]
resultat.append(sum(fenetre) / len(fenetre)) # bug 3 : diviser par la vraie taille
return resultatLa moyenne mobile lisse les mesures bruitées d’un capteur : vous l’utiliserez sur le robot. Un indice négatif dans une tranche compte depuis la fin : [-2:1] est vide, d’où une moyenne de 0 sans aucune erreur. Typique de la famille « logique ».
05 / Défis
Défi ★ — Division sécurisée et lecture tolérante
Consigne
1. diviser(a, b) renvoie a / b, ou None si b vaut 0, sans planter.
2. lire_nombres(chemin) lit un fichier ligne par ligne et renvoie la liste des lignes convertibles en float, en ignorant les autres (et en renvoyant une liste vide si le fichier n’existe pas).
Correction
def diviser(a, b):
try:
return a / b
except ZeroDivisionError:
return None
def lire_nombres(chemin):
nombres = []
try:
with open(chemin, encoding="utf-8") as f:
for ligne in f:
try:
nombres.append(float(ligne))
except ValueError:
pass
except FileNotFoundError:
pass
return nombresAttendu : None 2.0 puis [12.5, 7.0, 3.25] []. float(" 3.25 \n") fonctionne : la conversion ignore les espaces autour. Pour diviser, on aurait aussi pu écrire if b == 0: return None : quand le test est simple, il est préférable à un try.
05 / Défis
Défi ★★ — Tests pour un convertisseur d’unités
Consigne
Écrire convertir(valeur, de, vers) pour les unités « m », « cm », « mm », « km », qui lève ValueError si une unité est inconnue. Puis écrire test_convertir() avec au moins 6 assertions, dont un aller-retour (m → cm → m) et un cas d’unité inconnue.
Correction
EN_METRES = {"mm": 0.001, "cm": 0.01, "m": 1, "km": 1000}
def convertir(valeur, de, vers):
if de not in EN_METRES or vers not in EN_METRES:
raise ValueError(f"unité inconnue : {de} ou {vers}")
return valeur * EN_METRES[de] / EN_METRES[vers]
def test_convertir():
assert convertir(1, "m", "cm") == 100
assert convertir(2500, "m", "km") == 2.5
assert convertir(5, "km", "mm") == 5_000_000
assert abs(convertir(convertir(3.7, "m", "cm"), "cm", "m") - 3.7) < 1e-12
assert convertir(0, "mm", "km") == 0
try:
convertir(1, "m", "pouces")
except ValueError:
pass
else:
assert False, "unité inconnue acceptée"
print("convertir : OK")Un dictionnaire de facteurs vers une unité pivot remplace 16 if. Ajouter une unité = ajouter une ligne. C’est le genre de conception qu’on attend d’un bon programmeur.
05 / Défis
Défi ★★★ — Esprit prépa : le bug du siècle
Consigne
La recherche dichotomique (séance 11 en détail) trouve une valeur dans une liste triée en coupant en deux à chaque étape. La version ci-dessous a un bug célèbre, présent pendant 20 ans dans la bibliothèque standard de Java. En Python il ne se déclenche pas de la même façon, mais il y a ici un autre bug, plus classique : une boucle infinie pour certaines entrées.
1. Trouver une entrée qui bloque (attention : la page se fige — pensez d’abord, testez avec un compteur de sécurité).
2. Corriger. 3. Écrire des tests couvrant : présent au début, à la fin, au milieu, absent, liste vide, un élément.
Correction et ouverture
Avec [1, 3, 5, 7, 9] et cible 4 : bas=0, haut=4 → milieu 2 (5 > 4) haut=2 → milieu 1 (3 < 4) bas=1 → milieu 1 (3 < 4) bas=1… pour toujours. Quand haut = bas + 1, milieu = bas et l’intervalle ne rétrécit plus. Correction : bas = milieu + 1 et haut = milieu - 1 — on exclut le milieu déjà examiné, ce qui garantit que l’intervalle rétrécit strictement à chaque tour (c’est l’argument de terminaison qu’on attend en concours).
def dichotomie(liste, cible):
bas, haut = 0, len(liste) - 1
while bas <= haut:
milieu = (bas + haut) // 2
if liste[milieu] < cible:
bas = milieu + 1
elif liste[milieu] > cible:
haut = milieu - 1
else:
return milieu
return -1
def test_dichotomie():
l = [1, 3, 5, 7, 9]
assert dichotomie(l, 1) == 0 and dichotomie(l, 9) == 4 and dichotomie(l, 5) == 2
assert dichotomie(l, 4) == -1 and dichotomie(l, 100) == -1
assert dichotomie([], 3) == -1
assert dichotomie([3], 3) == 0 and dichotomie([3], 4) == -1
print("dichotomie : OK")Le bug Java (2006) : (bas + haut) / 2 débordait pour des tableaux de plus d’un milliard d’éléments, car les entiers y sont limités à 32 bits. Python n’a pas cette limite (séance 02), mais le C sur Arduino, si : un int y fait 16 bits, et 30 000 + 30 000 devient négatif. Vous y serez confronté en séance 22.
06 / Vérification
Qu’affiche try: x = int("5") except ValueError: print("A") else: print("B") ?
Deux questions supplémentaires
1. Quelle est la différence entre ValueError et TypeError ? TypeError : mauvais type ("a" + 1). ValueError : bon type, valeur impossible (int("a")).
2. Pourquoi un except: sans type est-il déconseillé ? Il masque toutes les erreurs, y compris celles que vous ne connaissez pas, et rend le débogage impossible.
Référence
Les mots à retenir
| Mot | Définition |
|---|---|
| Traceback | Trace des appels menant à l’erreur ; se lit de bas en haut. |
| Exception | Erreur signalée pendant l’exécution. |
try / except | Tenter, et réagir si une erreur survient. |
raise | Signaler soi-même une erreur. |
finally | Bloc exécuté quoi qu’il arrive. |
| Test | Code qui vérifie automatiquement un comportement attendu. |
| Bug logique | Résultat faux sans message d’erreur. |
Pour continuer
Vos programmes savent encaisser
Séance suivante : arrêter de tout réinventer. Les modules de Python et comment organiser votre propre code en fichiers.
À faire chez soi
- Reprendre un programme des séances précédentes et le rendre robuste à toute saisie.
- Écrire les tests de
est_premier(séance 05) et destatistiques(séance 06). - Sur votre PC : installer
pytest(pip install pytest), mettre les tests danstest_outils.py, lancerpytest.
Documentation
Tutoriel officiel : erreurs et exceptions · Hiérarchie des exceptions