Cette annexe est faite pour être ouverte au mauvais moment: celui où votre programme vient de s'arrêter sur un bloc de texte anglais que vous n'avez pas demandé. Elle rassemble les messages d'erreur rencontrés dans les dix chapitres du cours, dit ce que chacun signifie vraiment — ce qui n'est pas toujours ce qu'il a l'air de dire —, énumère ses deux ou trois causes habituelles, et donne pour chacun un programme minimal qui le produit et sa correction. Elle contient aussi la section la plus utile de toutes, et la seule qu'aucun message ne vous amènera à consulter: celle des erreurs qui ne lèvent rien. Un programme qui s'arrête vous dit où chercher; un programme qui affiche tranquillement un résultat faux ne vous dit rien du tout, et c'est lui qui coûte des heures.
Tous les messages reproduits ici sont authentiques. Chaque programme a été écrit dans un fichier, exécuté avec Python 3.13, et la sortie a été recopiée telle quelle. Une seule retouche, toujours la même: Python affiche le chemin complet du fichier (quelque chose comme /home/vous/travail/essai.py), et nous n'en gardons que le nom, pour que la ligne tienne dans la colonne. Numéros de ligne, soulignements, ponctuation et orthographe anglaise sont exactement ceux de l'interpréteur. Les chapitres suivent la même convention, à l'exception des chapitres 2 et 4, qui reproduisent le chemin de la machine sur laquelle ils ont été écrits: la différence ne porte que sur cette première ligne.
Lire une trace d'erreur
Le bloc que Python affiche
Quand une instruction ne peut pas s'exécuter, Python fabrique un objet appelé exception (chapitre 8), interrompt le programme sur-le-champ et imprime un bloc de texte: la trace d'appels (traceback). Ce bloc n'est pas une punition et ce n'est pas du bruit. C'est un diagnostic, souvent d'une précision remarquable, et il contient presque toujours la réponse.
Le réflexe à acquérir tient en trois mots: lisez-la à l'envers. La dernière ligne dit quoi; l'avant-dernier bloc dit où exactement; les blocs au-dessus disent comment on en est arrivé là.
Anatomie, sur une trace d'une seule entrée
Voici le cas le plus simple, celui d'une erreur survenue directement dans le corps du fichier. Le programme veut calculer l'aire d'un disque, et son auteur a écrit raton là où il avait nommé sa variable rayon.
# aire.py : l'aire d'un disque
rayon = 2.5
aire = 3.14159 * raton ** 2
print(aire)
Traceback (most recent call last):
File "aire.py", line 3, in <module>
aire = 3.14159 * raton ** 2
^^^^^
NameError: name 'raton' is not defined. Did you mean: 'rayon'?
Cinq éléments, toujours les mêmes, dans cet ordre:
- L'en-tête
Traceback (most recent call last):annonce qu'une exception a interrompu l'exécution et que les appels sont listés du plus ancien au plus récent. Il ne contient aucune information utile; passez-le. - Le fichier, entre guillemets. Tant que votre programme tient en un fichier, il n'y a rien à en tirer; à partir du chapitre 8, quand vous importerez vos propres modules, il vous dira lequel.
- Le numéro de ligne, ici 3, et le nom de la fonction, ici
<module>. Ce mot n'est pas un nom de fonction: il signifie «au niveau principal du fichier, en dehors de toute fonction». - La ligne de code, recopiée telle quelle, avec en dessous un soulignement qui désigne le morceau exact que l'interpréteur n'a pas su évaluer. Cette indication est apparue avec Python 3.11 et vaut de l'or: sans elle, une ligne longue vous laissait deviner.
- La dernière ligne: le type de l'exception, deux-points, le message. C'est la ligne à lire en premier.
Les deux sortes de soulignement
Python utilise deux caractères, et ils ne disent pas la même chose.
Les accents circonflexes ^ désignent la partie fautive. Les tildes ~ désignent le contexte de l'expression évaluée, c'est-à-dire ce sur quoi l'opération portait. Dans une addition impossible, les tildes soulignent les opérandes et les circonflexes l'opérateur:
age = "20"
print(age + 1)
Traceback (most recent call last):
File "essai.py", line 2, in <module>
print(age + 1)
~~~~^~~
TypeError: can only concatenate str (not "int") to str
Le ^ est sous le +: c'est l'addition qui a échoué. Les ~ sont sous age et sous 1: ce sont les deux valeurs en cause. Dans un accès indexé, la séparation est tout aussi nette:
notes = [4.5, 5.0, 3.5]
print(notes[3])
Traceback (most recent call last):
File "essai.py", line 2, in <module>
print(notes[3])
~~~~~^^^
IndexError: list index out of range
Les tildes couvrent notes, les circonflexes [3]: ce n'est pas la liste qui est fautive, c'est l'indice. Prenez l'habitude de regarder ce que les circonflexes couvrent avant de relire la ligne: neuf fois sur dix, ils pointent la moitié de l'expression à laquelle vous n'aviez pas pensé.
Un exemple à quatre entrées, décodé
Dès que vos programmes ont des fonctions (chapitre 4), la trace en compte plusieurs. Voici un programme qui lit des mesures au format suisse, avec une virgule décimale, et dont la seconde ligne de données est illisible. C'est celui du chapitre 8, privé de son try pour que l'exception aille jusqu'au bout.
# releves.py : lit des mesures au format suisse
def convertir(texte):
return float(texte.replace(",", "."))
def lire_ligne(ligne):
jour, texte = ligne.split(";")
return jour, convertir(texte)
def traiter(lignes):
for ligne in lignes:
print(lire_ligne(ligne))
traiter(["lundi;2,4", "mardi;n/d"])
('lundi', 2.4)
Traceback (most recent call last):
File "releves.py", line 16, in <module>
traiter(["lundi;2,4", "mardi;n/d"])
~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "releves.py", line 13, in traiter
print(lire_ligne(ligne))
~~~~~~~~~~^^^^^^^
File "releves.py", line 8, in lire_ligne
return jour, convertir(texte)
~~~~~~~~~^^^^^^^
File "releves.py", line 3, in convertir
return float(texte.replace(",", "."))
ValueError: could not convert string to float: 'n/d'
Décodons, dans l'ordre où il faut lire.
La dernière ligne, d'abord. ValueError: could not convert string to float: 'n/d'. Le type est ValueError: l'argument était du bon type — une chaîne, ce que float accepte parfaitement — mais sa valeur n'est pas convertible. Le message cite même la valeur coupable, 'n/d'. Vous savez déjà, en une ligne, ce qui s'est passé.
L'entrée du bas, ensuite. File "releves.py", line 3, in convertir: l'erreur a éclaté ligne 3, dans la fonction convertir. Notez qu'il n'y a pas de soulignement sous cette dernière ligne de code: Python ne souligne que ce qu'il peut localiser dans l'expression, et ici l'appel entier a échoué.
Les entrées au-dessus, si nécessaire. Elles racontent l'histoire, de haut en bas: la ligne 16 du fichier a appelé traiter; la ligne 13, dans traiter, a appelé lire_ligne; la ligne 8, dans lire_ligne, a appelé convertir. Cette chaîne est précieuse quand la fonction fautive est appelée depuis dix endroits différents: elle vous dit lequel des dix.
La ligne qui précède la trace. ('lundi', 2.4) a été affiché avant l'erreur. Ce n'est pas un détail: cela prouve que le premier tour de boucle s'est déroulé normalement et que la faute est dans la donnée, pas dans le programme.
Les erreurs qui n'ont pas de trace
Toutes les erreurs n'ont pas d'en-tête Traceback. Comparez:
File "essai.py", line 2
if temperature < 0
^
SyntaxError: expected ':'
Aucun Traceback (most recent call last):, et pour une bonne raison: il n'y a rien à retracer. Python vérifie la forme de tout le fichier avant d'en exécuter la première instruction; s'il ne sait pas le lire, il s'arrête là. Aucune ligne n'a tourné, pas même celles qui précèdent la faute et qui sont pourtant correctes. C'est une information de diagnostic à part entière.
- Pas d'en-tête
Traceback→SyntaxErrorouIndentationError→ le programme n'a pas commencé; cherchez une faute de frappe, pas une faute de raisonnement. Inutile d'ajouter desprintpour observer les variables: il n'y a pas encore de variables. - Un en-tête
Traceback→ le programme a tourné jusqu'à la ligne indiquée, et tout ce qui la précède a bien eu lieu; cherchez ce que contiennent vos variables à cet instant.
Une trace d'erreur commence directement par une ligne File, sans l'en-tête Traceback. Que pouvez-vous en déduire?
Un catalogue des erreurs du cours
Chaque entrée donne le message réel, ce qu'il signifie, ses causes habituelles, une reproduction minimale et sa correction. Les messages sont rangés par type d'exception, dans l'ordre où le cours les rencontre.
SyntaxError: Python n'a pas su lire votre texte
C'est la seule famille, avec IndentationError, qui se produit avant toute exécution. Le curseur ^ marque l'endroit où l'interpréteur a perdu le fil, et cet endroit est parfois après la vraie faute — typiquement une parenthèse ou un guillemet jamais refermés à la ligne précédente. Quand une SyntaxError désigne une ligne qui vous paraît irréprochable, regardez la ligne du dessus.
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
Ce que cela veut dire: vous avez écrit une affectation là où le langage attendait une expression. Concrètement: un seul signe égal dans la condition d'un if ou d'un while.
Causes habituelles: la confusion entre = qui donne un ordre et == qui pose une question (chapitre 2); plus rarement, un nom mal formé à gauche du =, comme note-max = 6.0, où Python lit une soustraction.
note = 3.5
if note = 4.0:
print("suffisant")
File "essai.py", line 2
if note = 4.0:
^^^^^^^^^^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
Corrigez la faute de syntaxe. La note vaut 3,5, donc le programme corrigé ne doit rien afficher du tout: c'est la bonne réponse, et elle est silencieuse.
Correction: if note == 4.0:. Bonne nouvelle, que le chapitre 2 souligne: en Python, il est impossible de confondre les deux dans un if sans être arrêté — en C, en Java ou en JavaScript, la même confusion passe silencieusement et produit un programme faux qui tourne.
SyntaxError: expected ':'
Ce que cela veut dire: une instruction composée — if, elif, else, while, for, def, try, except, with — a été écrite sans le deux-points qui annonce son bloc. Le curseur se place exactement là où il manquait.
temperature = -3.0
if temperature < 0
print("gel")
File "essai.py", line 2
if temperature < 0
^
SyntaxError: expected ':'
Il manque un caractère. Ajoutez-le pour que le programme affiche gel.
Correction: ajouter : en fin de ligne. C'est l'oubli le plus fréquent des premières semaines, parce que rien, dans l'habitude mathématique, ne prépare à terminer une condition par un signe de ponctuation.
SyntaxError: unterminated string literal (detected at line N)
Ce que cela veut dire: un guillemet ouvrant n'a pas de guillemet fermant sur la même ligne. Le ^ désigne le guillemet ouvrant, c'est-à-dire le début de la chaîne qui n'a jamais été refermée, et non l'endroit où vous auriez voulu la fermer.
message = "bonjour
print(message)
File "essai.py", line 1
message = "bonjour
^
SyntaxError: unterminated string literal (detected at line 1)
Fermez la chaîne pour que le programme affiche bonjour.
Causes habituelles: un guillemet oublié; un guillemet du même type à l'intérieur du texte ("il a dit "oui""), qui referme la chaîne trop tôt; un retour à la ligne dans une chaîne écrite avec des guillemets simples, qui exigerait des triples guillemets (chapitre 5).
SyntaxError: '(' was never closed
Ce que cela veut dire: une parenthèse, un crochet ou une accolade est resté ouvert. Le message est précieux parce qu'il désigne l'ouvrante, alors que le symptôme, lui, apparaît souvent des lignes plus bas.
total = (12 + 5
print(total)
File "essai.py", line 1
total = (12 + 5
^
SyntaxError: '(' was never closed
Correction: fermer la parenthèse. Retenez le mécanisme: à l'intérieur d'une parenthèse ouverte, Python autorise le passage à la ligne et poursuit sa lecture. C'est ce qui permet d'écrire une longue somme sur trois lignes (chapitre 1), et c'est aussi pourquoi une parenthèse oubliée fait déraper la lecture sur tout le reste du fichier.
IndentationError: le bloc n'est pas là où il devrait
IndentationError est une variété de SyntaxError, donc elle aussi arrête le fichier avant toute exécution. Retenez la symétrie de ses deux messages.
IndentationError: expected an indented block after 'if' statement on line N
Ce que cela veut dire: il manque une indentation. Un deux-points annonce un bloc, et rien n'est indenté derrière. Le message est remarquablement complet: il dit ce qu'il attendait, après quelle instruction, et à quelle ligne se trouvait cette instruction — tout en pointant la ligne où le bloc aurait dû commencer.
temperature = -3.0
if temperature < 0:
print("gel")
File "essai.py", line 3
print("gel")
^^^^^
IndentationError: expected an indented block after 'if' statement on line 2
Le bloc du if est vide. Indentez ce qui doit lui appartenir, pour que le programme affiche gel.
Correction: indenter la ligne 3 de quatre espaces. Le même message apparaît avec after 'for' statement, after 'while' statement ou after 'function definition': le mot qui suit after dit à quelle instruction le bloc manquant se rattache.
IndentationError: unexpected indent
Ce que cela veut dire: il y a une indentation de trop. Une ligne est décalée sans qu'aucun en-tête ne la commande.
temperature = -3.0
print("Bulletin")
print(temperature)
File "essai.py", line 3
print(temperature)
IndentationError: unexpected indent
Causes habituelles: un copier-coller qui a emporté des espaces; une ligne laissée indentée après avoir supprimé le if qui la commandait; un mélange d'espaces et de tabulations. Ce dernier cas donne un message à lui, TabError: inconsistent use of tabs and spaces in indentation, et il est pénible parce que deux lignes qui paraissent alignées à l'écran ne le sont pas pour l'interpréteur. Réglez votre éditeur une fois pour toutes sur quatre espaces.
NameError: ce nom n'existe pas
NameError: name 'raton' is not defined. Did you mean: 'rayon'?
Ce que cela veut dire: au moment où cette ligne s'exécute, aucune variable, aucune fonction et aucun module ne porte ce nom.
Les trois causes, par ordre de fréquence:
- une faute de frappe, de loin la plus courante. Python propose alors souvent le nom le plus ressemblant.
Did you mean:est une suggestion, pas un diagnostic: elle a souvent raison et parfois tort; - une variable utilisée avant d'avoir été affectée, par exemple parce que l'affectation est dans une branche
ifqui n'a pas été prise; - une majuscule:
noteetNotesont deux noms différents, ettruen'est pasTrue— écrireif reussi == true:donneNameError: name 'true' is not defined.
rayon = 2.5
aire = 3.14159 * raton ** 2
print(aire)
Traceback (most recent call last):
File "essai.py", line 2, in <module>
aire = 3.14159 * raton ** 2
^^^^^
NameError: name 'raton' is not defined. Did you mean: 'rayon'?
Une faute de frappe. Corrigez-la: le programme doit afficher l'aire du disque de rayon 2,5.
Un cas particulier mérite d'être connu: une variable créée à l'intérieur d'une fonction n'existe pas à l'extérieur (chapitre 4). Un NameError sur resultat après un appel qui a pourtant bien affecté resultat dans son corps n'est pas un mystère: l'espace local de l'appel a été abandonné au retour, et la valeur voulue doit sortir par un return.
TypeError: l'opération n'a pas de sens pour ces types
TypeError est la famille la plus nombreuse, et ses messages sont assez différents pour qu'il faille les traiter un par un.
TypeError: can only concatenate str (not "int") to str
Traduction: «je ne sais concaténer à une chaîne qu'une autre chaîne, or on me donne un entier». C'est l'erreur du texte qu'on prend pour un nombre.
Causes habituelles: l'oubli de la conversion après un input(), qui renvoie toujours une chaîne (chapitre 1); l'assemblage d'un message avec + au lieu d'une f-string (chapitre 5); un champ lu dans un fichier et jamais converti (chapitre 8).
age = "20"
print(age + 1)
Traceback (most recent call last):
File "essai.py", line 2, in <module>
print(age + 1)
~~~~^~~
TypeError: can only concatenate str (not "int") to str
Le programme veut ajouter 1 à un âge. Faites-le afficher 21 sans changer la première ligne.
Corrections, par ordre de qualité: convertir à la lecture, age = int(input("Votre age: ")); ou, s'il s'agit d'afficher, écrire print(f"{age} ans"), qui convertit tout seul. La variante TypeError: unsupported operand type(s) for -: 'str' and 'str' apparaît quand les deux opérandes sont des chaînes et que l'opération n'a aucun sens sur du texte.
TypeError: object of type 'NoneType' has no len()
Ce que cela veut dire: vous avez appliqué len() à None. Et None, dans un programme de ce cours, vient presque toujours de l'une de ces deux sources: une méthode qui modifie sur place et ne retourne rien, ou une fonction qui affiche au lieu de retourner.
notes = [4.5, 5.0, 3.5]
notes = notes.sort()
print(len(notes))
Traceback (most recent call last):
File "essai.py", line 3, in <module>
print(len(notes))
~~~^^^^^^^
TypeError: object of type 'NoneType' has no len()
Correction: notes.sort() seul sur sa ligne, ou triees = sorted(notes). La section suivante revient longuement sur cette faute, qui est la plus destructrice du cours: entre la ligne 2 et la ligne 3, la liste triée a purement et simplement disparu.
Deux messages de la même famille, produits par la même cause:
TypeError: 'NoneType' object is not subscriptable
qui signifie que vous avez écrit quelque_chose[0] sur un None, et
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
qui signifie que vous avez additionné None et un nombre. Chaque fois qu'un message parle de NoneType là où vous attendiez une valeur, cherchez une fonction qui affiche au lieu de retourner, ou une méthode de liste dont vous avez rangé le résultat quelque part.
TypeError: 'str' object does not support item assignment
Ce que cela veut dire: une chaîne est immuable (chapitre 5). La lecture mot[0] est autorisée, l'écriture mot[0] = "P" ne l'est pas — et ce n'est pas une IndexError, l'indice 0 étant parfaitement valide.
mot = "programme"
mot[0] = "P"
Traceback (most recent call last):
File "essai.py", line 2, in <module>
mot[0] = "P"
~~~^^^
TypeError: 'str' object does not support item assignment
Une chaîne ne se modifie pas. Obtenez quand même le mot avec une majuscule, en affichant Programme.
Correction: construire une nouvelle chaîne, mot = "P" + mot[1:]. Le message jumeau, TypeError: 'tuple' object does not support item assignment, dit exactement la même chose d'un tuple (chapitre 6): mot pour mot le même texte, au type près.
TypeError: perimetre() missing 1 required positional argument: 'largeur'
Ce que cela veut dire: l'appel ne fournit pas assez d'arguments. Le message nomme le paramètre manquant, ce qui est le plus court chemin vers la correction.
def perimetre(longueur, largeur):
return 2 * (longueur + largeur)
print(perimetre(3))
Traceback (most recent call last):
File "essai.py", line 5, in <module>
print(perimetre(3))
~~~~~~~~~^^^
TypeError: perimetre() missing 1 required positional argument: 'largeur'
Il manque un argument. Appelez la fonction avec une longueur de 3 et une largeur de 4, pour afficher le périmètre.
Le symétrique est TypeError: perimetre() takes 2 positional arguments but 3 were given. Les deux se corrigent en relisant la ligne def, et se raréfient dès qu'on nomme ses arguments à l'appel (chapitre 4).
TypeError: unhashable type: 'list'
Ce que cela veut dire: vous avez proposé une liste comme clé de dictionnaire ou comme élément d'ensemble (chapitre 7). Ce n'est pas un KeyError — la clé n'a même pas été cherchée; c'est une erreur de type, parce que l'objet proposé ne peut pas être une clé.
groupes = {}
groupes[["Alice", "Bruno"]] = "TP 1"
Traceback (most recent call last):
File "essai.py", line 2, in <module>
groupes[["Alice", "Bruno"]] = "TP 1"
~~~~~~~^^^^^^^^^^^^^^^^^^^^
TypeError: unhashable type: 'list'
Une liste ne peut pas servir de clé. Rangez le même groupe sous une clé acceptable, puis affichez le dictionnaire.
Correction: un tuple à la place de la liste, groupes[("Alice", "Bruno")] = "TP 1". La raison du refus vaut d'être retenue: une liste peut changer, donc son résumé numérique aussi, et le couple deviendrait introuvable alors même que vous tenez la clé en main.
ValueError: le type est bon, la valeur ne convient pas
La distinction avec TypeError est subtile et vaut la peine d'être comprise: int() accepte parfaitement qu'on lui donne une chaîne — c'est son travail —, donc ce n'est pas une erreur de type; mais cette chaîne-là ne contient pas de nombre, donc c'est une erreur de valeur.
ValueError: invalid literal for int() with base 10: 'trois'
texte = "trois"
nombre = int(texte)
Traceback (most recent call last):
File "essai.py", line 2, in <module>
nombre = int(texte)
ValueError: invalid literal for int() with base 10: 'trois'
La conversion échoue. Faites afficher nombre invalide au lieu d'interrompre le programme, sans changer la valeur de texte.
Trois causes, qui appellent trois corrections différentes:
- une saisie qui n'est pas un nombre. Ce n'est pas un bogue de votre programme, c'est un accident normal: enveloppez la conversion dans un
try/except ValueError(chapitre 8) et redemandez; - un nombre à virgule donné à
int():int("4.5")produit le même message avec'4.5'.intn'accepte qu'un entier écrit en chiffres. Écrivezint(float("4.5")), qui vaut 4, ouround(float("4.5")), qui vaut 4 également mais pour une autre raison; - une cellule vide dans un fichier:
int("")échoue aussi. C'est très fréquent dans un CSV, et c'est une bonne chose que Python le signale.
ValueError: could not convert string to float: '4,5'
Le même accident, côté float, avec la cause la plus typiquement suisse et française qui soit: la virgule décimale. Python ne connaît que le point.
nombre = float("4,5")
Traceback (most recent call last):
File "essai.py", line 1, in <module>
nombre = float("4,5")
ValueError: could not convert string to float: '4,5'
La virgule décimale française n'existe pas pour Python. Faites afficher 4.5.
Correction: float("4,5".replace(",", ".")). C'est ce que fait le chapitre 8 sur chaque champ d'un CSV exporté par un tableur suisse, où le séparateur de colonnes est le point-virgule précisément parce que la virgule est déjà prise.
ValueError: 2.0 is not in list
Ce que cela veut dire: la méthode index d'une liste a cherché une valeur qui n'y est pas. Contrairement à la méthode find des chaînes, qui renvoie -1 en cas d'échec, index lève une exception.
notes = [4.5, 5.0, 4.5]
print(notes.index(2.0))
Traceback (most recent call last):
File "essai.py", line 2, in <module>
print(notes.index(2.0))
~~~~~~~~~~~^^^^^
ValueError: 2.0 is not in list
index lève une erreur quand la valeur est absente. Faites afficher -1 dans ce cas, sans supprimer la recherche.
Correction: tester avant, if 2.0 in notes:. Le même message apparaît sur remove. Notez le piège inverse, décrit au chapitre 5: find ne lève rien et renvoie -1, qui est un indice parfaitement valide — une tranche construite sur un -1 non testé donne silencieusement toute la chaîne sauf son dernier caractère.
IndexError: l'indice sort de la séquence
IndexError: list index out of range, ou sa variante IndexError: string index out of range.
Ce que cela veut dire: la longueur n'est jamais un indice valide. Pour une séquence de éléments, les indices vont de 0 à du côté positif et de à du côté négatif; tout le reste lève l'erreur.
notes = [4.5, 5.0, 3.5]
print(notes[3])
Traceback (most recent call last):
File "essai.py", line 2, in <module>
print(notes[3])
~~~~~^^^
IndexError: list index out of range
Trois notes, et le programme en demande une quatrième. Faites-le afficher la dernière note, sans écrire le nombre 2 ni le nombre 3.
Les deux causes, toutes deux des erreurs «à un près» (off-by-one):
- un
+ 1de trop dans la boucle:for i in range(len(notes) + 1)déborde d'exactement un cran, au tout dernier tour, après avoir affiché tout le reste — ce qui la rend d'autant plus déroutante; - la comparaison avec le voisin: une boucle qui écrit
notes[i + 1]doit s'arrêter un cran plus tôt,range(len(notes) - 1). Chaque fois que votre boucle regarde le voisin, la borne recule d'autant.
KeyError: cette clé n'est pas dans le dictionnaire
KeyError: 'Zoe'
Ce que cela veut dire: vous avez demandé d[cle] pour une clé absente. Le message ne dit rien d'autre — mais il cite la clé, ce qui est souvent tout ce dont vous avez besoin.
notes = {"Alice": 4.5, "Bruno": 5.0}
print(notes["Zoe"])
Traceback (most recent call last):
File "essai.py", line 2, in <module>
print(notes["Zoe"])
~~~~~^^^^^^^
KeyError: 'Zoe'
Zoé n'est pas dans le carnet. Faites afficher 0.0 pour une clé absente, sans ajouter Zoé au dictionnaire.
Trois causes:
- la clé n'y est vraiment pas, et c'est un cas normal que votre programme doit traiter: employez
notes.get("Zoe", 0.0), qui rend une valeur par défaut sans rien modifier, ou testez avecif "Zoe" in notes:; - une différence invisible:
notes["alice"]etnotes["Alice"]ne sont pas la même clé, etnotes["Alice "], avec une espace finale, en est une troisième. Quand unKeyErrorvous paraît impossible, affichezlist(notes.keys())et comparez à l'œil; - la clé a été créée ailleurs, mal orthographiée. Souvenez-vous de l'asymétrie du chapitre 7: en lecture,
d["Alis"]lève unKeyErrorbien visible; en écriture,d["Alis"] = 4.5ne lève rien du tout et ajoute un onzième étudiant à votre carnet. Vérifiezlen(d)après avoir rempli un dictionnaire censé être complet.
KeyError apparaît aussi sur del d[cle], sur d.pop(cle) sans valeur par défaut, et sur ensemble.remove(x) pour un élément absent — la méthode discard, elle, ne proteste jamais.
ZeroDivisionError: division par zéro
Trois messages, selon l'opération et les types:
ZeroDivisionError: division by zero
ZeroDivisionError: float division by zero
ZeroDivisionError: integer modulo by zero
Le premier vient de 10 / 0, le deuxième de 10.0 / 0 ou 0.0 / 0, le troisième de 17 % 0. Ils disent tous la même chose.
total = 0.0
combien = 0
print(total / combien)
Traceback (most recent call last):
File "essai.py", line 3, in <module>
print(total / combien)
~~~~~~^~~~~~~~~
ZeroDivisionError: float division by zero
Aucune note n'a été saisie. Évitez la division par zéro et affichez Aucune note saisie.
La cause n'est presque jamais un zéro écrit en toutes lettres. C'est, dans l'immense majorité des cas, une moyenne sur un ensemble vide: un compteur resté à 0 parce qu'aucune donnée n'a été lue, un fichier sans ligne, un filtre qui n'a rien retenu.
Les deux corrections, selon l'intention:
if combien == 0:
print("Aucune note saisie.")
else:
print("Moyenne :", total / combien)
ou, quand la garde protège un calcul dans une condition, l'évaluation paresseuse du chapitre 2:
if x != 0 and 10 / x > 2:
print("Le quotient depasse 2.")
Cette dernière forme n'est sûre que dans cet ordre: and n'évalue son second opérande que si le premier est vrai. Inversez les deux et la division a lieu avant la garde.
AttributeError: cet objet n'a pas cette méthode
AttributeError: 'list' object has no attribute 'add'
Ce que cela veut dire: vous avez appelé une méthode qui n'existe pas pour ce type-là. Le message nomme le type et la méthode: c'est tout ce qu'il faut.
notes = [4.5, 5.0]
notes.add(3.5)
Traceback (most recent call last):
File "essai.py", line 2, in <module>
notes.add(3.5)
^^^^^^^^^
AttributeError: 'list' object has no attribute 'add'
add appartient aux ensembles, pas aux listes. Ajoutez la note et affichez la liste complète.
Les deux causes:
- une confusion de structure:
addest la méthode d'un ensemble,appendcelle d'une liste (chapitres 6 et 7). Le message vous dit lequel des deux vous avez réellement entre les mains, ce qui est souvent la vraie découverte. Le cas symétrique,AttributeError: 'dict' object has no attribute 'add', est celui du chapitre 7: vous croyiez avoir écrit un ensemble vide, mais{}est un dictionnaire vide, et l'ensemble vide s'écritset(); - une faute de frappe dans un nom de méthode:
nom.upercase()donneAttributeError: 'str' object has no attribute 'upercase'. Contrairement àNameError, Python ne propose pas toujours de correction ici.
Une troisième cause, plus retorse, revient au None déjà rencontré: AttributeError: 'NoneType' object has no attribute 'append' signifie que la variable sur laquelle vous appelez append vaut None — encore un liste = liste.sort() en amont.
FileNotFoundError: le fichier n'est pas là où vous croyez
FileNotFoundError: [Errno 2] No such file or directory: 'temperatures.txt'
with open("temperatures.txt", "r") as fichier:
print(fichier.readline())
Traceback (most recent call last):
File "essai.py", line 1, in <module>
with open("temperatures.txt", "r") as fichier:
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'temperatures.txt'
Le fichier n'existe pas. Faites en sorte que le programme affiche fichier absent et continue, au lieu de s'arrêter.
Lisez le message avec attention: Python vous redit exactement le chemin qu'il a essayé. S'il ne comporte aucun dossier, c'est qu'il a cherché dans le dossier de travail, c'est-à-dire celui depuis lequel vous avez lancé le programme — et non celui qui contient le fichier .py.
Trois causes, dans l'ordre:
- le dossier de travail n'est pas celui que vous croyez. C'est de loin le cas le plus fréquent, et le fichier existe bel et bien «juste là». La fonction
getcwd()du moduleosaffiche le dossier réel en une ligne; - une faute dans le nom, extension comprise:
notes.txtcontrenotes.csv, une majuscule, un accent; - le fichier n'a jamais été créé, parce que le programme qui devait l'écrire a échoué avant.
Quand l'absence est une situation normale — une sauvegarde qui n'existe pas encore —, la bonne réponse n'est pas de corriger le chemin mais de traiter le cas:
try:
with open("sauvegarde.txt", "r") as fichier:
contenu = fichier.read()
except FileNotFoundError:
contenu = ""
Notez enfin que seul le mode "r" lève cette erreur: les modes "w" et "a" créent le fichier s'il manque.
RecursionError: la descente ne s'arrête pas
RecursionError: maximum recursion depth exceeded
Ce que cela veut dire: plus de mille appels de fonction étaient imbriqués en même temps, et CPython refuse d'aller plus loin. La ligne qui précède le message est la signature du problème: [Previous line repeated 996 more times].
def somme_ratee(liste):
if len(liste) == 0:
return 0
return liste[0] + somme_ratee(liste)
print(somme_ratee([1, 2, 3]))
Traceback (most recent call last):
File "essai.py", line 7, in <module>
print(somme_ratee([1, 2, 3]))
~~~~~~~~~~~^^^^^^^^^^^
File "essai.py", line 4, in somme_ratee
return liste[0] + somme_ratee(liste)
~~~~~~~~~~~^^^^^^^
File "essai.py", line 4, in somme_ratee
return liste[0] + somme_ratee(liste)
~~~~~~~~~~~^^^^^^^
File "essai.py", line 4, in somme_ratee
return liste[0] + somme_ratee(liste)
~~~~~~~~~~~^^^^^^^
[Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded
La récursion ne descend jamais. Réparez-la pour que le programme affiche la somme de la liste.
Les deux causes, et il faut savoir les distinguer parce qu'elles donnent le même message:
- le cas de base est absent, ou jamais atteint. Une fonction sans
ifd'arrêt; ou unif n == 0appelé avecn = -1, qui descend vers , et ne rencontre jamais 0. Écrivezif n <= 0; - le cas récursif ne réduit pas le problème. C'est le programme ci-dessus: le cas de base
len(liste) == 0est parfait, il ne sera simplement jamais rencontré, puisque l'appel reçoit la même liste. L'oubli d'un[1:]suffit.
Il existe une troisième situation, où la fonction est juste: une récursion correcte mais trop profonde, par exemple une somme récursive sur 2000 éléments. sys.setrecursionlimit existe et n'est presque jamais la bonne réponse; réécrivez la fonction avec une boucle (chapitre 9).
Le réflexe de relecture tient en deux questions écrites côte à côte: quel est mon cas de base? et qu'est-ce qui diminue à chaque appel? Si vous ne pouvez pas nommer la grandeur qui diminue — un entier, une longueur, une largeur d'intervalle —, votre fonction ne se terminera pas.
AssertionError: le programme s'est pris en faute
AssertionError, souvent toute seule, sans message.
def arrondi_demi_point(note):
return round(note * 2) / 2
assert arrondi_demi_point(4.25) == 4.5
Traceback (most recent call last):
File "essai.py", line 5, in <module>
assert arrondi_demi_point(4.25) == 4.5
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError
L'assertion échoue alors que la fonction semble juste. Trouvez pourquoi, puis réparez-la pour que 4,25 s'arrondisse bien à 4,5.
Ce que cela veut dire: une affirmation que vous avez écrite s'est révélée fausse. Ce n'est pas Python qui a un problème, c'est votre programme qui vient de vous prévenir — c'est exactement le service qu'on lui demandait.
Ici, la fonction est fausse et l'assertion a raison: round de Python arrondit au pair le plus proche en cas d'égalité parfaite, donc round(8.5) vaut 8 et non 9. La correction est int(note * 2 + 0.5) / 2 (chapitre 4).
Deux conseils:
- ajoutez un message, après une virgule:
assert f(20, 40) == 4, "la moitie des points doit donner 4". La dernière ligne de la trace porte alors une phrase française au lieu d'un mot anglais; - n'utilisez pas
assertpour valider une saisie d'utilisateur. Un programme lancé avec l'option-Osupprime toutes les assertions, et le contrôle disparaît sans prévenir.assertdit «mon code est faux si ceci n'est pas vrai»; unifavec un message dit «votre saisie ne convient pas».
Le soulignement est ici particulièrement utile: les ^ couvrent toute l'expression testée, ce qui permet de la recopier à l'invite interactive et de voir ce qu'elle vaut réellement.
UnboundLocalError: le nom est local, et pas encore affecté
UnboundLocalError: cannot access local variable 'appels' where it is not associated with a value
C'est le message le plus déroutant du cours, parce qu'il paraît absurde: la variable existe, elle vaut 0, elle est juste au-dessus.
appels = 0
def enregistrer(note):
appels = appels + 1
return note
print(enregistrer(4.5))
Traceback (most recent call last):
File "essai.py", line 9, in <module>
print(enregistrer(4.5))
~~~~~~~~~~~^^^^^
File "essai.py", line 5, in enregistrer
appels = appels + 1
^^^^^^
UnboundLocalError: cannot access local variable 'appels' where it is not associated with a value
Le compteur est lu avant d'exister. Réparez le programme pour qu'il affiche 4.5, en gardant le compteur à jour.
Voici ce qui se passe réellement. Avant d'exécuter une fonction, Python lit tout son corps et dresse la liste des noms qui y sont affectés. Ces noms-là seront locaux pour tout l'appel — y compris dans les lignes qui précèdent l'affectation. Or appels est affecté à la ligne 5, donc appels est local dans enregistrer, et l'évaluation du membre de droite demande la valeur de ce appels local, qui n'en a pas encore.
La règle, en une phrase: lire une variable globale depuis une fonction est autorisé; l'affecter la rend locale dans toute la fonction.
La correction n'est pas global appels, qui fonctionne mais rend la fonction intestable, illisible depuis l'appel et non réutilisable. C'est la transformation du chapitre 4: ce qui entre devient un paramètre, ce qui sort devient une valeur de retour.
def enregistrer(note, deja_vues):
return deja_vues + 1
vues = 0
vues = enregistrer(4.5, vues)
vues = enregistrer(5.0, vues)
print(vues)
2
Tableau récapitulatif
| Message (début) | Traduction en une phrase | Où chercher |
|---|---|---|
SyntaxError: invalid syntax. Maybe you meant '==' | un = dans une condition | la condition du if ou du while |
SyntaxError: expected ':' | deux-points manquant | la fin de la ligne d'en-tête |
SyntaxError: unterminated string literal | guillemet non refermé | le guillemet ouvrant désigné par le ^ |
SyntaxError: '(' was never closed | parenthèse non refermée | la ligne désignée, et celles du dessous |
IndentationError: expected an indented block | il manque une indentation | la ligne qui suit le deux-points |
IndentationError: unexpected indent | il y a une indentation de trop | la ligne désignée |
NameError: name '…' is not defined | ce nom n'existe pas ici | faute de frappe, majuscule, portée locale |
TypeError: can only concatenate str | du texte pris pour un nombre | la conversion oubliée après input |
TypeError: … 'NoneType' … | une valeur qui vaut None | une méthode sur place, ou un print au lieu d'un return |
TypeError: 'str' object does not support item assignment | une chaîne est immuable | construire une nouvelle chaîne |
TypeError: … missing 1 required positional argument | argument manquant à l'appel | la ligne def |
TypeError: unhashable type: 'list' | une liste ne peut pas être une clé | remplacer la liste par un tuple |
ValueError: invalid literal for int() | cette chaîne n'est pas un entier | la saisie, ou int au lieu de float |
ValueError: could not convert string to float | cette chaîne n'est pas un nombre | une virgule décimale, une cellule vide |
ValueError: … is not in list | index n'a pas trouvé | tester avec in d'abord |
IndexError: list index out of range | l'indice dépasse la longueur | un + 1 de trop, ou le voisin |
Un programme de 40 lignes s'arrête sur une trace comportant trois entrées File, toutes dans votre fichier. Combien de ces entrées désignent la ligne où l'erreur a réellement éclaté?
Les erreurs qui ne lèvent rien
Voici la section la plus utile de cette annexe, et la seule qu'aucun message ne vous amènera à ouvrir. Les erreurs qui suivent ne produisent aucune trace: le programme se déroule jusqu'au bout, affiche un résultat plausible, et ce résultat est faux. Elles coûtent infiniment plus cher que les précédentes, parce qu'on ne les cherche pas. Elles ont toutes le même symptôme — «mon programme tourne mais le chiffre est faux» — et se reconnaissent à leurs signatures; apprenez-les comme on apprend un catalogue de maladies, par le tableau clinique.
notes = notes.sort() — la ligne qui détruit les données
C'est la plus destructrice du cours. sort() trie la liste sur place et retourne None. L'affectation range donc None dans notes, et la liste triée, que plus aucun nom ne désigne, disparaît.
notes = [4.5, 5.0, 3.5]
notes = notes.sort()
print(notes)
None
Ce programme affiche None. Réparez-le pour qu'il affiche la liste triée, et gardez notes utilisable après.
Ici, l'erreur finit par se voir, une ou deux lignes plus loin, sous la forme d'un TypeError parlant de NoneType. Mais elle peut aussi ne jamais se voir, si la variable n'est plus utilisée que dans un affichage. Règle de survie: une méthode de liste dont le nom est un verbe à l'impératif — sort, reverse, append, insert, extend, remove — modifie et retourne None; ne mettez jamais son résultat dans une variable.
notes = [4.5, 5.0, 3.5]
notes.sort() # sur place, sur sa propre ligne
triees = sorted(notes) # ou bien: une nouvelle liste
Le tableau des retours, à connaître par cœur: toutes les méthodes de modification retournent None, sauf pop, qui retourne l'élément retiré.
L'alias qu'on prenait pour une copie
liste2 = liste1 ne copie rien. C'est un deuxième nom collé sur une seule liste; toute modification faite par l'un est vue par l'autre.
notes = [4.5, 5.0, 3.5]
sauvegarde = notes
notes[0] = 1.0
print(sauvegarde)
print(sauvegarde is notes)
[1.0, 5.0, 3.5]
True
La «sauvegarde» n'en est pas une: modifier notes la modifie aussi. Réparez-la pour que le programme affiche la liste d'origine puis False.
La «sauvegarde» n'a rien sauvegardé. Le symptôme est toujours le même — «une variable a changé alors que je ne l'ai pas touchée» — et il vaut surtout pour une liste passée en argument à une fonction: le paramètre est un nouveau nom collé sur la liste de l'appelant, pas une copie.
notes = [4.5, 5.0, 3.5]
sauvegarde = list(notes) # une vraie deuxieme liste
notes[0] = 1.0
print(sauvegarde)
print(notes)
[4.5, 5.0, 3.5]
[1.0, 5.0, 3.5]
Le geste de diagnostic tient en une ligne: print(a is b). S'il répond True, vous avez un alias. Et pour une liste de listes, list(...) ne suffit pas: il faut copy.deepcopy (chapitre 6).
Modifier une liste pendant qu'on la parcourt
notes = [4.5, 3.0, 3.5, 2.5, 5.0]
for note in notes:
if note < 4:
notes.remove(note)
print(notes)
[4.5, 3.5, 5.0]
Le programme veut ne garder que les notes suffisantes, et en oublie une. Réparez-le pour qu'il affiche exactement les notes supérieures ou égales à 4.
Il reste 3,5, qui est pourtant insuffisante. La boucle for maintient un indice interne, invisible, qu'elle incrémente à chaque tour; quand un élément est retiré, tout ce qui le suivait glisse d'un cran vers la gauche, mais l'indice interne, lui, avance quand même. L'élément qui vient de prendre la place du supprimé est donc sauté. Aucune erreur, aucun avertissement, un résultat faux. La correction la plus sûre ne retire rien: elle construit.
notes = [4.5, 3.0, 3.5, 2.5, 5.0]
reussites = [note for note in notes if note >= 4]
print(reussites)
[4.5, 5.0]
Sur un dictionnaire, la même faute est heureusement bruyante: Python lève RuntimeError: dictionary changed size during iteration. Parcourez list(notes.keys()), qui fige la liste des clés avant de commencer.
0.1 + 0.2 n'est pas 0.3
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(abs((0.1 + 0.2) - 0.3) < 1e-9)
0.30000000000000004
False
True
Ce n'est pas un bogue de Python: tous les langages qui utilisent les nombres à virgule flottante de la norme IEEE 754 donnent ce résultat. Un dixième n'a pas d'écriture finie en base deux, pas plus qu'un tiers n'en a en base dix.
L'écart, de l'ordre de , est sans la moindre portée pratique sur la valeur. Il a une portée totale sur le test d'égalité, qui répond False. C'est le test qu'il faut changer, pas le calcul: abs(a - b) < 1e-9 remplace a == b chaque fois que a ou b est le résultat d'un calcul en virgule flottante. Ce cours emploie cet idiome partout, à partir du chapitre 2.
Le piège se déguise volontiers: une mention refusée à un étudiant dont la moyenne calculée vaut 4.449999999999999 au lieu de 4.45; une équation du second degré déclarée sans solution parce que son discriminant théoriquement nul sort du calcul à ; une boucle while x != 1.0: qui ajoute 0.1 dix fois et ne s'arrête jamais. Les trois sont dans les chapitres 1, 2 et 3, avec leurs sorties.
Deux corollaires: pour de l'argent, comptez en centimes entiers, qui sont exacts; et les entiers ne trahissent jamais — 1 + 2 == 3 est vrai pour toujours, sans tolérance et sans discussion.
Un except: nu qui avale une faute de frappe
def moyenne(valeurs):
try:
return somme(valeurs) / len(valeurs)
except:
return 0.0
print(moyenne([2.4, 5.1, 7.8]))
0.0
Le programme affiche 0.0 alors qu'il devrait afficher une moyenne. Un except: nu cache une faute de frappe: trouvez-la et réparez le programme.
La moyenne de 2,4, 5,1 et 7,8 vaut 5,1, et le programme affiche 0.0 sans broncher. La fonction somme n'existe pas — c'est sum —, Python lève une NameError, et l'except: nu l'attrape comme le reste. La faute de frappe est devenue un résultat faux, et un résultat faux ne se remarque pas.
Nommez l'exception, et la même faute se dénonce d'elle-même: avec except ZeroDivisionError:, la NameError remonte et la trace vous désigne somme en deux secondes. Règle: un except nomme toujours le type qu'il attend, et le plus précis possible. Le corollaire vaut aussi pour except Exception:, sa variante à peine moins dangereuse.
// là où il fallait /
total = 45.5
moyenne = total // 10
print(moyenne)
print(total / 10)
4.0
4.55
Le programme affiche 4.0 au lieu de la vraie moyenne. Corrigez l'opérateur.
La division entière n'a pas produit d'erreur; elle a jeté la partie décimale. 4.0 au lieu de 4.55: une moyenne de classe fausse d'un demi-point, affichée avec l'aplomb d'un nombre juste.
Le signal qui doit alerter: un résultat décimal qui tombe systématiquement rond. Une moyenne toujours entière, un pourcentage toujours multiple de 1, un prix sans centimes — allez regarder les barres de division. La question à se poser avant de choisir: est-ce que je compte quelque chose, ou est-ce que je mesure quelque chose? On compte avec //, on mesure avec /. Notez que // entre deux flottants rend un flottant — 45.5 // 10 vaut 4.0 et non 4 —, ce qui rend le diagnostic encore moins évident.
Une fonction qui affiche là où elle devrait retourner
def moyenne(valeurs):
print(sum(valeurs) / len(valeurs))
resultat = moyenne([4.5, 5.0, 3.5])
print(resultat)
4.333333333333333
None
Le programme affiche la moyenne, puis None. Faites en sorte que resultat contienne vraiment la moyenne, et qu'elle ne soit affichée qu'une fois.
La bonne valeur est bien passée à l'écran — et elle est perdue. La fonction ne contient pas de return, donc elle rend None. Tant que le programme se contente d'afficher, tout semble marcher; le jour où l'on veut additionner trois moyennes, écrire un fichier ou tester la fonction, plus rien ne fonctionne.
Le test qui départage, du chapitre 4: après l'appel, la valeur existe-t-elle encore quelque part dans le programme? Si la fonction a seulement affiché, non. Corrigez en return sum(valeurs) / len(valeurs) et déplacez le print chez l'appelant. La règle: calculez dans des fonctions qui retournent, affichez dans une fonction qui affiche, et ne mélangez pas les deux.
Le décalage d'un cran de range
notes = [4.5, 5.0, 3.5, 6.0, 4.0, 5.5, 4.5, 3.0, 5.0, 4.5]
total = 0.0
for i in range(1, len(notes)):
total = total + notes[i]
print(total / len(notes))
print
4.1
4.55
La moyenne affichée est fausse d'une note. Corrigez le parcours.
La boucle part de 1 au lieu de 0: le premier élément n'a jamais été additionné, et la moyenne annoncée, 4,1, est fausse d'un demi-point. Aucune erreur, aucun indice hors bornes — juste une note manquante sur dix.
C'est l'erreur «à un près» sous sa forme silencieuse, et elle a trois visages: range(1, n) au lieu de range(n), qui perd le premier; range(n) là où il fallait range(n + 1), qui perd le dernier; et range(1, 10) écrit en pensant «de 1 à 10», qui n'en donne que neuf.
Les deux réflexes du chapitre 3: pour faire tours, écrivez range(n) — ne comptez pas les valeurs, lisez l'argument, il est le nombre de tours; et pour aller de à inclus, écrivez range(a, b + 1), avec le + 1 visible, donc vérifiable. Le contrôle qui l'attrape à coup sûr coûte une ligne: faites afficher le nombre d'éléments réellement traités et comparez-le à len(notes).
Le tableau des symptômes
| Symptôme observé | Cause la plus probable |
|---|---|
| une variable a changé sans que j'y touche | un alias: b = a sur une liste, ou une liste passée en argument |
None là où j'attendais une valeur | liste = liste.sort(), ou une fonction qui affiche au lieu de retourner |
| un élément sur deux a été sauté | une liste modifiée pendant son propre parcours |
un test d'égalité répond False alors que c'est visiblement égal | deux flottants comparés avec == |
| un résultat décimal toujours rond | // au lieu de / |
| un zéro ou une valeur de repli inexpliquée | un except: nu qui a avalé une erreur de programmation |
| une somme ou une moyenne fausse d'un élément | une borne de range décalée d'un cran |
| un maximum qui ne figure pas dans les données | un maximum initialisé à 0 sur des données négatives |
Votre programme affiche une moyenne de classe qui tombe toujours sur un nombre entier, alors que les notes vont par quarts de point. Quelle est la piste la plus probable?
Une méthode de déverminage
Le déverminage (debugging) n'est pas un talent, c'est une procédure. En voici une, courte, qui traite l'immense majorité des cas. Suivez-la dans l'ordre; la tentation, quand un programme échoue, est de sauter directement à l'étape 4, et c'est ce qui fait perdre le plus de temps.
1. Lisez la dernière ligne, d'abord. Le type de l'exception donne la catégorie de la faute, le message donne laquelle. Cette ligne à elle seule résout un cas sur deux. Si le message est en anglais et que vous ne le comprenez pas, cherchez-le dans le catalogue ci-dessus avant de faire quoi que ce soit d'autre.
2. Trouvez votre propre fichier dans la trace. Descendez jusqu'à la dernière entrée File qui porte le nom d'un de vos fichiers, et allez à la ligne indiquée. Regardez ce que les accents circonflexes couvrent. S'il n'y a pas d'en-tête Traceback, ne cherchez pas de variable: le fichier n'a pas été exécuté, la faute est typographique.
3. Reproduisez avec la plus petite entrée possible. Un programme qui échoue sur un fichier de mille lignes ne se dévermine pas sur mille lignes. Réduisez: trois notes au lieu de dix, deux lignes de fichier au lieu de deux cents, un seul appel au lieu d'une boucle. Ou bien l'erreur disparaît, et ce que vous venez de retirer contenait la cause; ou bien elle persiste sur cinq lignes, et il devient difficile de ne pas la voir. Recopier la ligne suspecte à l'invite interactive >>> avec des valeurs simples est la forme la plus rapide de cette réduction, et c'est le réflexe le plus rentable du cours.
4. Affichez ce que contiennent vraiment vos variables. Juste avant la ligne fautive, insérez
print("DEBUG", type(x), repr(x))
type(x) répond «est-ce bien un nombre?», et neuf fois sur dix la surprise est là: une chaîne là où vous attendiez un nombre. repr(x) montre ce qu'il y a vraiment dans la variable, guillemets et caractères invisibles compris — c'est ainsi qu'on découvre qu'une ligne lue dans un fichier vaut '2.4\n' et non '2.4'. Le mot DEBUG en tête sert à retrouver et à supprimer ces lignes ensuite.
Dans une boucle, faites imprimer la trace: une ligne par tour, avec le numéro du tour et l'état de chaque variable. C'est l'outil de déverminage le plus efficace qui existe et il ne demande qu'un print. Dans une fonction, une assertion vaut mieux qu'un affichage, parce qu'elle reste:
assert len(valeurs) > 0, "moyenne d'une liste vide"
5. Ne corrigez qu'une chose à la fois, et relancez. Corriger trois choses d'un coup rend impossible de savoir laquelle était la bonne, et il arrive plus souvent qu'on ne croit que deux des trois «corrections» aient cassé autre chose. Après chaque modification, relancez et vérifiez sur un cas dont vous connaissez déjà la réponse: le carnet de notes du cours, par exemple, doit donner une somme de 45,5 et une moyenne de 4,55. Un programme se vérifie sur des données dont on sait d'avance ce qu'elles doivent produire.
La règle du canard en plastique
Il existe une technique dont l'efficacité est hors de proportion avec son sérieux apparent, et qui porte le nom de débogage par le canard en plastique (rubber duck debugging): expliquez votre programme à voix haute, ligne par ligne, à quelqu'un qui ne peut pas vous aider — un camarade qui ne connaît pas le sujet, ou un objet posé sur votre bureau.
La raison pour laquelle cela marche est la même que celle qui fait la valeur des assertions et des docstrings. Lire son propre code silencieusement, c'est lire ce qu'on a voulu écrire; le dire à voix haute oblige à énoncer ce qu'il fait réellement, phrase par phrase. Dans la grande majorité des cas, on s'interrompt soi-même au milieu d'une phrase — «ici je parcours toutes les notes… enfin, à partir de la deuxième» — et l'on n'a jamais eu besoin d'interlocuteur. Deux variantes du même principe: écrire la docstring d'une fonction avant son corps, et écrire trois assertions avant de faire confiance au code.
Ce qu'il ne faut pas faire
- Modifier au hasard jusqu'à ce que ça marche. Un programme qui «marche» sans qu'on sache pourquoi cessera de marcher sans qu'on sache pourquoi non plus. Si vous ne pouvez pas dire quelle était la cause, vous ne l'avez pas corrigée.
- Ajouter un
try/exceptpour faire taire l'erreur. Attraper une exception n'annule pas l'opération qui a échoué: la valeur attendue n'a pas été produite, et le programme continuera avec autre chose. Unexceptse justifie quand on sait quoi faire du cas, pas quand on veut faire disparaître un message. - Relire le programme du début en espérant voir la faute. Vous relirez ce que vous avez voulu écrire. La trace vous a donné un numéro de ligne: allez-y.
- Croire que l'erreur est dans Python. Ce n'est jamais le cas. C'est une pensée qui coûte cher, parce qu'elle arrête la recherche.
- Soupçonner l'ordinateur d'avoir changé d'avis. Si le même programme donne deux résultats différents sur les mêmes données, c'est que les données ne sont pas les mêmes, ou qu'une variable a survécu d'une exécution à l'autre dans l'invite interactive. Relancez dans un interpréteur neuf avant de conclure quoi que ce soit.
Une dernière chose, qui n'est pas une technique. Un message d'erreur n'est pas un jugement sur vous: c'est la partie de Python qui travaille pour vous. Un programmeur expérimenté ne fait pas moins d'erreurs qu'un débutant — il en fait à peu près autant, et il les lit plus vite.
C'est tout ce que cette annexe cherche à vous apprendre.
Références
- Downey, A., Think Python, 3e éd., O'Reilly, Sebastopol — l'annexe «Debugging», ainsi que les sections de déverminage qui terminent chaque chapitre. Librement disponible en ligne; une traduction française existe.
- Swinnen, G., Apprendre à programmer avec Python 3, Eyrolles, Paris — les passages consacrés aux messages d'erreur et à la gestion des exceptions. Librement disponible en ligne.
- Matthes, E., Python Crash Course, 3e éd., No Starch Press, San Francisco — chapitre 10, «Files and Exceptions», pour la lecture des traces et le choix du type à intercepter.
- La documentation officielle Python, The Python Tutorial, section «Errors and Exceptions», pour la syntaxe complète de
try/except/else/finallyet la hiérarchie des types. - La documentation officielle Python, The Python Standard Library, section «Built-in Exceptions», qui est la liste faisant foi de tous les types d'exception et de leur signification exacte.
- La documentation officielle Python, What's New in Python 3.11, section «Enhanced error locations in tracebacks», qui décrit les soulignements
~et^introduits par cette version.