Débogage A : ce qu'est un bug, les types d'erreurs et la lecture d'une stack trace
- Définir ce qu’est un bug et distinguer une Syntax Error, une Runtime Error et une Logical Error
- Lire une Stack Trace — identifier le type d’erreur, le message, le fichier et la ligne
- Connaître les erreurs courantes de Python et la cause de chacune
- Comprendre pourquoi une Logical Error est la plus difficile à trouver
Qu’allons-nous apprendre ?
- Ce qu’est un bug.
- Les trois types d’erreurs : Syntax Error, Runtime Error, Logical Error.
- Comment lire une Stack Trace (Traceback), le message d’erreur de Python.
- Les erreurs les plus courantes, et ce que dit chacune.
Pourquoi en avons-nous besoin ?
Les développeurs passent une part énorme de leur temps à trouver et corriger des bugs. La différence entre un débutant et un professionnel n’est pas que le professionnel n’a pas de bugs, mais qu’il lit le message d’erreur et sait en quelques secondes où regarder. Les débutants voient beaucoup de texte rouge et paniquent. À la fin de la leçon, le texte rouge deviendra une carte.
Explication à partir des bases
Qu’est-ce qu’un bug ?
Un bug est un écart entre ce que le programme est censé faire et ce qu’il fait réellement. Parfois le programme plante, et parfois il « fonctionne » mais renvoie un résultat faux.
En 1947, les ingénieurs de l’ordinateur Mark II ont trouvé un papillon de nuit (un insecte, « bug » en anglais) coincé dans l’un des relais, et l’ont collé dans le journal de travail avec la légende « first actual case of bug being found ». Le mot servait déjà pour les pannes techniques avant cela, mais cette histoire l’a rendu célèbre.
Les trois types d’erreurs
| Type | Quand est-elle découverte | Python la signale-t-il ? | Exemple |
|---|---|---|---|
| Syntax Error | Avant l’exécution (lors de la vérification de la syntaxe) | Oui, et aucune ligne ne s’exécute | Un : ou une parenthèse manquante |
| Runtime Error (Exception) | Pendant l’exécution, sur la ligne problématique | Oui, et les lignes précédentes se sont déjà exécutées | Division par 0, une variable qui n’existe pas |
| Logical Error | Jamais automatiquement | Non ! Le code s’exécute « avec succès » | + au lieu de -, une condition inversée |
Vous vous souvenez du thème sur l’ordinateur, le système d’exploitation et le réseau ? Une Syntax Error est détectée quand Python traduit en bytecode, avant l’exécution. Une Runtime Error se produit quand la PVM exécute une ligne précise et échoue.
Une Logical Error est la plus dangereuse : pas de message, pas de couleur rouge, juste un résultat faux. On ne la trouve que si l’on vérifie que le résultat est correct.
La Stack Trace — la carte de l’erreur
Quand une Runtime Error se produit, Python affiche un Traceback (aussi appelé Stack Trace) : le chemin que le code a suivi jusqu’à l’erreur.
Traceback (most recent call last):
File "shop.py", line 9, in <module>
total = checkout(cart)
File "shop.py", line 5, in checkout
return subtotal / count
ZeroDivisionError: division by zeroLisez-la de bas en haut :
- La dernière ligne — le type de l’erreur (
ZeroDivisionError) et le message (division by zero). Commencez toujours ici. - La ligne au-dessus — où exactement : le fichier
shop.py, ligne 5, dans la fonctioncheckout, et le code lui-même. - Encore au-dessus — qui a appelé cette fonction : la ligne 9, au niveau supérieur du fichier (
<module>).
Pourquoi « Stack » ? Parce que chaque appel de fonction est empilé sur la pile des appels (la Call Stack). Le Traceback montre la pile au moment du plantage, du premier appel (en haut) au dernier (en bas).
Les erreurs courantes en Python
| Erreur | Signification | Cause fréquente |
|---|---|---|
SyntaxError | Le code n’a pas une syntaxe valide | Un :, des parenthèses ou des guillemets manquants |
IndentationError | Mauvaise indentation | Mélange d’espaces, indentation incohérente |
NameError | Un nom n’est pas défini | Une faute de frappe, une utilisation avant la définition |
TypeError | Une opération sur un type inadapté | "5" + 3, appeler une fonction avec le mauvais nombre d’arguments |
ValueError | Le bon type, une valeur invalide | int("hello") |
ZeroDivisionError | Division par zéro | Un dénominateur qui n’a pas été vérifié |
IndexError | Un indice en dehors de la liste | items[5] quand il y a 3 éléments |
KeyError | Une clé qui n’existe pas dans un dict | (nous l’apprendrons dans le thème structures de données et fonctions) |
AttributeError | L’objet n’a pas cet attribut/cette méthode | "abc".push(), une faute de frappe dans un nom de méthode |
FileNotFoundError | Un fichier n’existe pas | Un mauvais chemin, le Working Directory |
Syntaxe
Il n’y a pas de nouvelle syntaxe. L’outil, c’est la lecture. Le modèle de tout Traceback :
Traceback (most recent call last):
File "<file>", line <N>, in <function or <module>>
<the code on that line>
... ← more frames, each a function call
<ErrorType>: <message> ← START READING HEREEt celui d’une Syntax Error (sans Traceback, car rien ne s’est exécuté) :
File "<file>", line <N>
<the code>
^ ← where Python noticed the problem
SyntaxError: <message>Premier exemple de code
week-2/bug_report.py :
def average(numbers):
total = sum(numbers)
return total / len(numbers)
scores = []
print("Average score:", average(scores))Sortie :
Traceback (most recent call last):
File "C:\dev\kodkod6\week-2\bug_report.py", line 7, in <module>
print("Average score:", average(scores))
^^^^^^^^^^^^^^^
File "C:\dev\kodkod6\week-2\bug_report.py", line 3, in average
return total / len(numbers)
~~~~~~^~~~~~~~~~~~~~
ZeroDivisionError: division by zeroAnalyse du code ligne par ligne
Lecture du Traceback de bas en haut :
| Ligne de la sortie | Ce qu’elle nous dit |
|---|---|
ZeroDivisionError: division by zero | Ce qui s’est passé : une division par zéro. |
line 3, in average + return total / len(numbers) | Où : ligne 3, dans average. Le ~~~^~~~ marque exactement l’opération de division. |
line 7, in <module> + average(scores) | Qui a appelé : ligne 7, au niveau supérieur. <module> = « pas dans une fonction ». |
La conclusion : len(numbers) vaut 0, la liste est donc vide. Et qui a passé une liste vide ? La ligne 7 a passé scores, qui vaut []. Le vrai bug est que nous n’avons pas traité le cas d’une liste vide. La ligne qui a planté (3) est le symptôme, et décider quoi faire d’une liste vide est la correction :
def average(numbers):
if len(numbers) == 0:
return 0
return sum(numbers) / len(numbers)Un autre exemple
Les trois types d’erreurs sur la même fonction. La fonction doit calculer un prix après une remise en pourcentage.
Syntax Error :
def price_after_discount(price, percent)
return price - price * percent / 100 File "discount.py", line 1
def price_after_discount(price, percent)
^
SyntaxError: expected ':'Il manque les deux-points. Python n’a exécuté aucune ligne.
Runtime Error :
def price_after_discount(price, percent):
return price - price * percent / 100
print(price_after_discount("100", 10))Traceback (most recent call last):
File "discount.py", line 4, in <module>
print(price_after_discount("100", 10))
File "discount.py", line 2, in price_after_discount
return price - price * percent / 100
~~~~~~~~~~~~~~~~^~~~~
TypeError: unsupported operand type(s) for /: 'str' and 'int'La syntaxe est valide, mais à l’exécution price est du texte. "100" * 10 « fonctionne » (cela répète le texte 10 fois !), mais ensuite la division par 100 échoue. Le ^ pointe vers le /.
Logical Error :
def price_after_discount(price, percent):
return price - percent / 100
print(price_after_discount(100, 10))99.9Aucune erreur ! Mais une remise de 10 % sur 100 devrait renvoyer 90.0. Nous avons oublié de multiplier par price. Python ne peut pas savoir ce que nous voulions dire. Seule une vérification par rapport à un résultat attendu le révélera.
Erreur courante
Erreur 1 : lire le Traceback depuis le haut, et paniquer. Commencez toujours par la dernière ligne : le type et le message. Puis la ligne au-dessus. La plupart du temps, c’est tout ce dont vous avez besoin.
Erreur 2 : corriger la ligne qui a planté, au lieu de la cause. Dans le premier exemple, une « correction » comme return total / (len(numbers) + 1) fait disparaître l’erreur, mais renvoie un résultat faux dans tous les cas ! Cela a transformé une Runtime Error en Logical Error. Encore pire.
Erreur 3 : chercher la Syntax Error uniquement sur la ligne indiquée dans le message. Parfois le problème est sur la ligne précédente :
prices = [10, 20, 30
total = sum(prices) File "test.py", line 1
prices = [10, 20, 30
^
SyntaxError: '[' was never closedPython est arrivé à la ligne 2, et a découvert que le crochet de la ligne 1 n’avait jamais été fermé. Dans les versions récentes, le message est précis, mais en général : si la ligne marquée semble correcte, vérifiez celle d’avant.
Exercices
Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.
Exercice 1: Quel type d’erreur ?
Pour chaque extrait — s’agit-il d’une SyntaxError, d’une Runtime Error ou d’une Logical Error ? Et quel est le problème ?
# A
if age > 18
print("adult")
# B
print(total_price) # total_price was never defined
# C
def area(width, height):
return width + height # should be the area of a rectangleAfficher la solution
- A — SyntaxError : il manque les deux-points
:à la fin de la ligneif. Aucune ligne ne s’exécute. - B — Runtime Error (
NameError) : la syntaxe est valide, mais à l’exécution Python ne trouve pas de variable nomméetotal_price. - C — Logical Error : il n’y a aucune erreur, mais le résultat est faux — l’aire se calcule par une multiplication,
width * height. On ne trouve cette erreur que si l’on vérifie le résultat.
Exercice 2: Lire une Stack Trace
Traceback (most recent call last):
File "shop.py", line 12, in <module>
print(checkout([20, 30], 0))
File "shop.py", line 7, in checkout
return average_price(prices, count)
File "shop.py", line 3, in average_price
return sum(prices) / count
ZeroDivisionError: division by zero- Quel est le type de l’erreur et quel est le message ?
- Dans quel fichier, à quelle ligne et dans quelle fonction s’est-elle produite ?
- D’où venait la valeur problématique ?
Afficher la solution
ZeroDivisionError: division by zero— une division par zéro. On lit d’abord la dernière ligne.- Dans le fichier
shop.py, ligne 3, dans la fonctionaverage_price— c’est la ligne la plus basse de la liste. - On lit vers le haut :
average_pricea été appelée depuis la ligne 7 danscheckout, etcheckouta été appelée depuis la ligne 12 aveccount = 0. Le problème a donc commencé dans l’appel de la ligne 12, qui a passé 0.
Exercice 3: Un bug logique dans une conversion de température
def to_celsius(fahrenheit):
return fahrenheit - 32 * 5 / 9
print(to_celsius(212))
print(to_celsius(32))Les résultats devraient être 100 et 0, mais autre chose sort. Trouvez le bug et corrigez-le.
Résultat attendu :
100.0
0.0Afficher la solution
def to_celsius(fahrenheit):
return (fahrenheit - 32) * 5 / 9
print(to_celsius(212))
print(to_celsius(32))100.0
0.0L’ordre des opérations : la multiplication et la division passent avant la soustraction, donc le code d’origine calculait fahrenheit - (32 * 5 / 9). Les parenthèses obligent la soustraction à se faire d’abord. C’est une Logical Error — pas de message d’erreur, juste un résultat faux.
Exercice 4: TypeError
age = 25
print("Age: " + age)- Quelle erreur obtiendrez-vous, et pourquoi ?
- Corrigez-la de deux façons différentes.
Résultat attendu :
Age: 25Afficher la solution
TypeError: can only concatenate str (not "int") to str— on ne peut pas joindre du texte et un nombre avec+.- Deux façons :
age = 25
print("Age: " + str(age))Age: 25age = 25
print("Age:", age)Age: 25La première convertit le nombre en texte. La seconde passe deux valeurs à print, qui ajoute un espace entre elles.
Exercice 5: La moyenne d’une liste vide
def average(numbers):
return sum(numbers) / len(numbers)
print(average([80, 90]))
print(average([]))- Que se passera-t-il lors du second appel, et quelle erreur obtiendrez-vous ?
- Corrigez la fonction pour qu’elle renvoie 0 pour une liste vide.
Résultat attendu :
85.0
0Afficher la solution
len([])vaut 0, vous obtenez doncZeroDivisionError: division by zero— une erreur d’exécution.- Vérifiez ce cas avant la division :
def average(numbers):
if len(numbers) == 0:
return 0
return sum(numbers) / len(numbers)
print(average([80, 90]))
print(average([]))85.0
0Résumé
- Bug = un écart entre le comportement voulu et le comportement réel.
- Syntax Error — avant l’exécution, aucune ligne ne s’exécute. Runtime Error — au milieu de l’exécution, avec un Traceback. Logical Error — pas de message, juste un résultat faux.
- Lisez un Traceback de bas en haut : type + message → ligne et fonction → qui a appelé.
- La ligne qui a planté n’est parfois que le symptôme. Cherchez la cause.
- Les Logical Errors ne se découvrent qu’en comparant un résultat avec une valeur attendue.