Traquer les erreurs

Traquer les erreurs #

\(\)

Ce document regroupe des cas d’erreurs courantes, plus ou moins simples à détecter, ainsi que les solutions à mettre en œuvre pour les régler.

Les messages d’erreur donnés ici sont ceux obtenus avec Python 3.11. Les messages d’erreur ont été particulièrement améliorés en Python 3.10 et Python 3.11, vous n’aurez donc peut être pas exactement les mêmes.

Erreurs immédiates (Exception levée, et traceback) #

Certaines erreurs sont détectées à la lecture du code, et d’autres à l’exécution. Les erreurs de syntaxe et d’indentation sont détectées à la lecture du code. En ce qui concerne les erreurs levées à l’exécution du code, elles peuvent passer inaperçues lors d’une exécution (si le code concerné n’est pas exécuté ou que les valeurs en entrée ne conduisent pas à l’erreur).

SyntaxError #

Les causes sont multiples :

  • Oubli ou mauvais placement d’un délimiteur : parenthèse, crochet, guillemet
  • Oubli des : pour annoncer un bloc. Certaines instructions (if, elif, else, for, def, while pour les plus courantes) sont suivies d’un bloc de code. Elles doivent être suivies de : puis de lignes indentées délimitant le bloc.
  • Caractère manquant ou qui ne peut pas se trouver là (un chiffre en début de nom de variable, un caractère spécial comme ! à un endroit impossible…)

Ces erreurs sont détectées à la lecture du code source (avant qu’il ne soit réellement exécuté).

Certaines instructions peuvent courir sur plusieurs lignes. Il n’est donc pas rare qu’une erreur de syntaxe soit détectée et donc signalée sur la ligne qui suit l’endroit où elle se trouve réellement.

■ Exemple 1

x = (3 + 4) * (4 + (1 * 3)
y = 2
  File "<stdin>", line 1
    x = (3 + 4) * (4 + (1 * 3)
                   ^^^^^^^^^^
SyntaxError: invalid syntax. Perhaps you forgot a comma?

Correction :

x = (3 + 4) * (4 + (1 * 3))
y = 2

■ Exemple 2

texte = 'Python à l'école'
  line 1
    texte = 'Python à l'école'
                             ^
SyntaxError: unterminated string literal (detected at line 1)

L’apostrophe avant école est prise pour l’apostrophe fermante de celle avant Python. L’apostrophe après école n’est donc pas refermée…

Il y a plusieurs corrections possibles. La meilleure est indiquée en premier.

texte = "Python à l'école"
texte = 'Python à l''école'
texte = 'Python à l\'école'

■ Exemple 3

a = 5
if a == 4
    print("a vaut 4")
File "<stdin>", line 2
    if a == 4
             ^
SyntaxError: expected ':'

Correction :

a = 5
if a == 4:
    print("a vaut 4")

IndentationError #

Certaines instructions sont suivies de :, puis d’un bloc indenté. La valeur préconisée pour l’indentation est de 4 espaces, mais elle n’est pas imposée (c’est une mauvaise idée d’indenter avec autre chose que 4 espaces, mais ce n’est pas une faute).

Les erreurs d’indentation peuvent être de 3 types :

  • le bloc est indenté sans raison : unexpected indent
  • pas de bloc indenté alors qu’il devrait y en avoir un : expected an indented block
  • après un bloc indenté, le niveau d’indentation trouvé ne correspond à aucun bloc englobant : unindent does not match any outer indentation level

Une erreur assez courante et pénible à détecter (qui arrive de moins en moins souvent car les éditeur de code la gèrent bien) est de mélanger les indentations avec 4 espaces et avec une tabulation. Ce sont deux niveaux d’indentation différents, alors que visuellement, ils sont similaires sur l’écran.

Ces erreurs sont détectées à la lecture du code, avant même son exécution.

■ Exemple 1

for k in range(10):
print("Bonjour")
  File "<stdin>", line 2
    print("Bonjour")
    ^
IndentationError: expected an indented block after 'for' statement on line 1

L’indentation du bloc dans la boucle for a été oubliée :

for k in range(10):
    print("Bonjour")

■ Exemple 2

a = 3
    b = 5
  File "<stdin>", line 1
    b = 5
IndentationError: unexpected indent

Le bloc b = 5 a été indenté sans raison :

a = 3
b = 5

■ Exemple 3

for x in (0, 1, 2):
    for y in (0, 1, 2):
        print(x, y)
  print("Boucle y terminée")
  File "<stdin>", line 4
    print("Boucle y terminée")
                              ^
IndentationError: unindent does not match any outer indentation level

La ligne print("Boucle y terminée") est mal indentée : elle n’est ni dans la boucle for x (auquel cas, elle devrait être alignée avec for y...), ni après la boucle for x (auquel cas, elle devrait être alignée avec for x...).

Sa place est après la boucle for y, dans la boucle for x :

for x in (0, 1, 2):
    for y in (0, 1, 2):
        print(x, y)
    print("Boucle y terminée")

NameError #

Le nom de variable (ou de fonction) n’est pas connu de Python. Ce type d’erreur est souvent dû à une faute de frappe dans un nom, ou à une variable non initialisée.

■ Exemple 1

lst = [5, 6, 7]
for v in lst:
    s = s + v
print(s)
Traceback (most recent call last):
  File "<tmp 1>", line 3, in <module>
    s = s + v
NameError: name 's' is not defined

La variable s n’a pas été initialisée avant sa première utilisation. Correction :

s = 0
lst = [5, 6, 7]
for v in lst:
    s = s + v
print(s)

■ Exemple 2

liste = [4, 5, 6]
for v in l1ste:
    print(v)
Traceback (most recent call last):
  File "<tmp 1>", line 2, in <module>
    for v in l1ste:
NameError: name 'l1ste' is not defined

La variable liste a été mal orthographiée. Correction :

liste = [4, 5, 6]
for v in liste:
    print(v)

■ Exemple 3

def pdt_des_elts(lst):
    p = 1
    for v in lst:
        p = p * v
    return p

lst = [1, 2, 3, 4, 5]
print(pdt_des_elements(lst))
Traceback (most recent call last):
  File "<tmp 1>", line 8, in <module>
    print(pdt_des_elements(lst))
NameError: name 'pdt_des_elements' is not defined

Le nom de la fonction a été mal orthographié. Correction :

lst = [1, 2, 3, 4, 5]
print(pdt_des_elts(lst))

TypeError #

Ce type d’erreur est par exemple obtenu si on essaie de faire une opérations sur des objets dont le type ne convient pas :

  • additionner une chaîne et un nombre (ou toute autre erreur similaire)
  • indicer une séquence avec autre chose qu’une tranche (slice) ou un entier
  • passer un nombre incorrect de paramètres à une fonction
  • utilisation de parenthèses (callable) lorsque ça n’a pas de sens
  • utilisation de crochets (subscriptable) lorsque ça n’a pas de sens

■ Exemple 1

a = "une fois sur"
b = 2
print(a + b)
Traceback (most recent call last):
  File "<tmp 1>", line 3, in <module>
    print(a + b)
TypeError: can only concatenate str (not "int") to str

L’opération de concaténation (+) ne peut concaténer qu’un str avec un str mains ne peut pas concaténer un int à un str. Correction :

a = "une fois sur"
b = 2
print(a + str(b))

■ Exemple 2

a = 10
lst = [1, 2, 3, 4, 5]
print(lst[a / 2])
Traceback (most recent call last):
  File "<tmp 3>", line 3, in <module>
    print(lst[a / 2])
TypeError: list indices must be integers or slices, not float

Les indices des listes (entre [ ]) peuvennt être des entiers, ou des slices (comme 0:12:2), mais pas des float (des nombres à virgule). L’utilisation de la division / donne un float comme résultat (que la division tombe juste ou non) et pas un int.

Le problème signalé ci-dessus n’est pas un indice hors limite (IndexError). Il y a bien un problème potentiel d’indice hors limite, mais l’exception signalée est levée avant que le problème de l’indice hors limite ne se produise).

Correction (il y aura alors un autre problème d’indice hors limite IndexError, mais c’est un autre problème) :

a = 10
lst = [1, 2, 3, 4, 5]
print(lst[a // 2])

■ Exemple 3

lst = [5, 6, 7]
print(lst(0))
Traceback (most recent call last):
  File "<tmp 1>", line 3, in <module>
    print(lst(0))
TypeError: 'list' object is not callable

L’interpréteur indique que le type list (ici lst) n’est pas callable. Seuls les objets callable (par exemple les fonctions, les classes…) peuvent être suivis de parenthèses. Correction :

lst = [5, 6, 7]
print(lst[0])

■ Exemple 4

def mafonction(n):
    return n ** 2 + 2 * n + 1

print(mafonction[5])
Traceback (most recent call last):
  File "<tmp 1>", line 5, in <module>
    print(mafonction[5])
TypeError: 'function' object is not subscriptable

L’interpréteur indique que le type function (ici mafonction) n’est pas subscriptable. Seuls les objets subscriptables (par exemple les listes, les tuples, les dictionnaires…) peuvent être suivis d’une paire de crochets. Correction :

def mafonction(n):
    return n ** 2 + 2 * n + 1

print(mafonction(5))

■ Exemple 5

import math
print(math.pow(3, 4, 5))
Traceback (most recent call last):
  File "<tmp 3>", line 2, in <module>
    print(math.pow(3, 4, 5))
TypeError: pow expected 2 arguments, got 3

Dans le cas de TypeError, le message d’erreur (par exemple « pow expected 2 arguments, got 3 » ci-dessus) permet souvent de mieux circonscrire le problème. Il faut donc le lire et le comprendre. Il est ici indique que la fonction pow attend deux argument. Or on lui en a fourni 3. Il existe une autre fonction pow, dans builtins qui prend 3 arguments… Corrections :

import math
print(math.pow(3, 4))

ou

print(pow(3, 4, 5))

IndexError #

Cette erreur indique qu’un indice (d’une liste par exemple) est hors limite. On rappelle que les indices des séquences (liste, tuple, chaîne) commencent à 0 et se terminent à n - 1 si n est la longueur de la séquence.

■ Exemple 1

lst = [1, 2, 3]
i = 0
while i < 4:
    print(lst[i])
    i = i + 1
Traceback (most recent call last):
  File "<tmp 1>", line 4, in <module>
    print(lst[i])
IndexError: list index out of range

On est «sorti» de la liste, l’indice indique une case en dehors de la liste. Correction possible :

lst = [1, 2, 3]
i = 0
while i < 3:
    print(lst[i])
    i = i + 1

C’est par ailleurs une mauvaise idée de parcourir une liste avec une boucle while plutôt que for (cf Bonnes pratiques et joli code).

Erreurs indirectes #

Profitez de vos erreurs. Si vous en faites une, le plus important n’est pas de la corriger, mais de savoir pourquoi la faute dans le code a provoqué ce que vous avez constaté. C’est généralement plus difficile que simplement corriger, mais on apprend plus de choses ainsi.

Ne laissez jamais une erreur inexpliquée, même si vous avez réussi à la corriger.

ififelse n’est pas une instruction #

Deux if qui se suivent sont deux instructions séparées (alors qu’un elif qui suit un if fait partie de la même instruction).

Si on veut mettre l’appréciation A pour une note supérieure ou égale à 15, C pour une note strictement inférieurs à 10 et B sinon, il ne faut pas écrire :

if note >= 15:
    appreciation = "A"
if note < 10:
    appreciation = "C"
else:
    appreciation = "B"

En écrivant ça, personne ne pourra obtenir l’appréciation A, même en ayant 20.

Le code correct est :

if note >= 15:
    appreciation = "A"
elif note < 10:
    appreciation = "C"
else:
    appreciation = "B"

sort et sorted #

Certaines fonctionnalités sont disponibles par le biais de méthodes et de fonctions. C’est le cas pour le tri d’une liste.

On peut écrire lst.sort() qui fait muter la liste lst qui se retrouve alors triée (ici, .sort() est une méthode appliquée sur un objet de type list). L’appel à lst.sort() ne renvoie rien.

Mais on peut aussi écrire sorted(lst), qui ne fait pas muter lst, mais renvoie une copie triée (ici sorted est une fonction qui prend en paramètre un objet de type list).

Exemple :

# Utilisation de sort
lst = [1, 5, 4, 2, 7]
lst.sort()
print(lst) # Maintenant lst est triée
# Utilisation de sorted
lst = [1, 5, 4, 2, 7]
lst2 = sorted(lst)
print(lst2) # lst2 est triée, mais lst ne l'est pas
# Utilisation de sorted
lst = [1, 5, 4, 2, 7]
lst = sorted(lst)
print(lst) # lst est triée (mais ce n'est pas le même lst qu'avant, vérifiez l'id)

Une erreur très courante est d’écrire :

lst = lst.sort() # Maintenant dans lst, il y a None

ou encore :

return lst.sort() # Renvoie None à tous les coups

On a la même chose (pas tout à fait, mais presque) avec lst.reverse() et reversed(lst).

Raccourcis mal employés #

Le raccourci += est connu… mais parfois mal utilisé. Le code suivant ne calcule pas la somme des entiers de 1 à 10 (et aucune erreur de syntaxe n’est signalée, puisqu’il n’y a pas d’erreur de syntaxe)

s = 0
for i in range(1, 11):
    s =+ i
print(s) # affiche 10 au lieu de 55

N’utilisez pas les raccourcis, sauf si vous êtes sûrs de ne jamais vous tromper, sinon, écrivez sans raccourci :

s = 0
for i in range(1, 11):
    s = s + i
print(s) # affiche 55

Autre exemple, on vérifie que la fonction random produit effectivement 50 % de nombres entre 0 et 0.5 et 50 % entre 0.5 et 1 :

# verification moisie 
import random
p, g = 0, 0
for k in range(1000):
    if random.random() < 0.5:
        p += 1
    else:
        g =+ 1
print("Pourcentage < 0.5 : ", round(p / (p + g) * 100, 2))
print("Pourcentage > 0.5 : ", round(g / (p + g) * 100, 2))

On obtient le magnifique résultat scientifique (rassurez-vous la génération de nombres pseudo-aléatoire fonctionne mieux que ça…) :

Pourcentage < 0.5 :  99.81
Pourcentage > 0.5 :  0.19
N’utilisez jamais ces raccourcis sauf si vous êtes certains de ne jamais vous tromper.

Étourderie #

L’oubli du mot range dans une boucle répéter pour donne (parfois) une instruction syntaxiquement correcte, et l’erreur est donc pénible à détecter :

for i in (2, 10, 2):
    print(i)

au lieu de :

for i in range(2, 10, 2):
    print(i)

Une variable ne contient pas une formule… #

Le programme suivant est une tentative pour lister les multiples de 3 dont le carré est inférieur à 10000 (mais il ne fonctionne pas) :

n = 0
carre = n ** 2
while  carre < 1e4:
    n = n + 3

L’erreur se solde par une boucle infinie, ce qui ne passe pas inaperçu. Un raisonnement simple sur la boucle permet de trouver l’erreur.

Bien entendu, on a simplement oublié de mettre à jour la valeur de la variable carre, ce qu’il est nécessaire de faire à chaque fois que la valeur de n change.

n = 0
carre = n ** 2
while  carre < 1e4:
    n = n + 3
    carre = n ** 2

Copies superficielles ou profondes #

La copie des types simples ne pose pas de problème particulier. Il faut en revanche être prudent avec les collections. On peut copier une collection ainsi :

a = [1, 2, [5, 6, 7]]
b = list(a)

Mais c’est une copie superficielle (shallow copy). Si un des éléments est modifiable (si un des éléments de la liste est une liste par exemple), ça peut être un problème. Le test suivant permet de comprendre ce qui se passe :

>>> a = [1, [1, 2]]
>>> id(a), id(a[0]), id(a[1])
    (140409461203840, 140409515960624, 140409461264192)
>>> b = list(a)
>>> id(b), id(b[0]), id(b[1]) 
    (140409460884416, 140409515960624, 140409461264192) # b[0] et b[1] partagés
>>> a, b
    ([1, [1, 2]], [1, [1, 2]])
>>> b[0] = 42
>>> a, b
    ([1, [1, 2]], [42, [1, 2]])   # contenu de a non modifié
>>> b[1][0] = 54
>>> a, b
    ([1, [54, 2]], [42, [54, 2]]) # contenu (indirect) de a modifié

Pour obtenir un comportement différent, on dispose d’un module copy qui offre une copie en profondeur (deep copy) :

>>> import copy
>>> a = [1, [1, 2]]
>>> b = copy.deepcopy(a)
>>> b[1][0] = 54
>>> a, b
    ([1, [1, 2]], [1, [54, 2]])

tuple non modifiable mais… #

Un tuple n’est pas modifiable. Mais il peut contenir une liste qui l’est (ce qui n’est pas forcément une bonne idée…) :

>>> l = (3, 4, [1, 2, 3])
>>> l[2].append(4)
>>> l
   (3, 4, [1, 2, 3, 4])

⚠ Évaluation paresseuse #

Certaines fonctions renvoient des itérateurs. C’est le cas de reversed, qui ne renvoie pas directement une séquence à l’envers mais un itérateur qui peut parcourir les éléments en sens inverse. Mais attention aux pièges :

>>> t = [1, 2, 3]
>>> u = reversed(t) # le contenu n'est pas encore parcouru
>>> t[1] = 10 
>>> v = list(u)     # il est parcouru maintenant
>>> v
    [3, 10, 1]
>>> t = [1, 2, 3]
>>> it = reversed(t)
>>> u = list(it)    # le contenu est parcouru, it est épuisé
>>> v = list(it)    # plus rien dans it...
>>> u 
    [3, 2, 1]
>>> v
    []

Exercices 😀 #

⚠ L’erreur n’est pas là #

Dans un programme Python, la ligne où une exception est levée, et donc une erreur révélée, n’est pas forcément la ligne qui contient la faute réelle.

Voici un exemple. On dispose d’une liste, contenant des mots et leur classement (peu importe ce qu’est ce classement). Par exemple :

data =  [ (1, "Minerva"), (2, "Albus"), (3, "Severus"), (4, "Sibylle"), (5, "Gilderoy") ]

Puis, on dispose d’une fonction, qui prend en paramètre le nom d’une personne, et renvoie le nombre de voyelles que contient ce mot :

def nb_voyelles(nom: str) -> int:
    nom_m = nom.lower()
    for voyelle in "aeiou":
        c = c + nom_m.count(voyelle)
    return c

À présent, pour chaque nom de la liste data, on souhaite afficher le nombre de voyelles qu’il contient :

for nom in data:
    print(nb_voyelles(nom))

Si on l’exécute, on obtient le traceback suivant qui indique une erreur sur la ligne nom_m = nom.lower() :

Traceback (most recent call last):
  File "<tmp 1>", line 11, in <module>
    print(nb_voyelles(nom))
  File "<tmp 1>", line 4, in nb_voyelles
    nom_m = nom.lower()
AttributeError: 'tuple' object has no attribute 'lower'

Pourtant l’erreur est ailleurs. Saurez-vous expliquer ce qui se passe ?

Code source complet
data =  [ (1, "Minerva"), (2, "Albus"), (3, "Severus"), (4, "Sibylle"), (5, "Gilderoy") ]

def nb_voyelles(nom: str) -> int:
    nom_m = nom.lower()
    c = 0
    for voyelle in "aeiou":
        c = c + nom_m.count(voyelle)
    return c

# Affiche le nombre de voyelles contenu dans chaque non
for nom in data:
    print(nb_voyelles(nom))

⚠ Méli mélo de noms de variables #

Une erreur assez courante, et parfois difficile à détecter, est la réutilisation d’un même nom de variable pour deux choses différentes. Par exemple, lorsqu’on traite les composantes rouge vertes et bleues d’une image, et qu’il faut parcourir toute l’image (balayage selon les abscisses et le ordonnées), on voit parfois :

from PIL import Image

image = Image.open("sample_image.png")

for a in range(image.size[0]):
    for b in range(image.size[1]):
        r, v, b = image.getpixel((a, b))
        # Traitement pour assombrir par exemple : 
        r1, v1, b1 = r//2, v//2, b//2
        image.putpixel((a, b), (r1, v1, b1))

image.show()

Si on exécute le code fourni, avec l’image dans le répertoire courant, aucune exception n’est levée. Et l’image résultat s’affiche. Mais elle ne ressemble pas à ce qu’on devrait obtenir (original à gauche, image transformée à droite) :

À noter que le code échoue sur une image plus petite (par exemple sur une image 128x128). L’exception levée signale alors :

Traceback (most recent call last):
  File "<tmp 2>", line 10, in <module>
    image.putpixel((a, b), (r1, v1, b1))
  File "/usr/lib/python3.9/site-packages/PIL/Image.py", line 1758, in putpixel
    return self.im.putpixel(xy, value)
IndexError: image index out of range

Pourtant même sur une petite image, le code peut ne pas lever d’exception… à condition que l’image soit suffisamment sombre…

Voyez-vous où se situe le problème ?

⚠ Paramètres par défaut mutables #

Utiliser un paramètre par défaut mutable est syntaxiquement correct, mais c’est une erreur de programmation dans la plupart des cas.

Voici l’exemple d’une fonction (jouet) qui prend en paramètre une chaîne. Elle compte les voyelles et les consonnes, puis les ajoute à un éventuel décompte préalable passé en paramètre (une liste de deux entiers) :

>>> compte_lettres("Wingardium")
    [4, 5] # 4 consonnes et 5 voyelles

>>> compte_lettres("Wingardium", [6, 10]) # Ajout au décompte précédent
    [10, 15] 

Pour faire ceci, on utilise un paramètre dont la valeur par défaut est [0, 0] (qui est une liste, et donc qui est mutable…)

import string
# Attention, ce code est mauvais...
def compte_lettres(chaine, compte=[0, 0]):
    for c in chaine.lower():
        if c in "aeiou":
            compte[0] += 1
        elif c in string.ascii_lowercase:
            compte[1] += 1
    return compte

Mais tout ne se passe pas comme prévu :

>>> compte_lettres("Leviosa", [1, 1])
    [5, 4] 

>>> compte_lettres("Leviosa") 
    [4, 3] 

>>> compte_lettres("Leviosa") 
    [8, 6]       # <=========== Pas normal.... :(

Vous êtes invités à réfléchir aux causes du problème. En attendant, la bonne pratique est d’utiliser uniquement des paramètres par défaut non mutables :

import string
def compte_lettres(chaine, compte=None):
    if compte is None:
        compte = [0, 0]
    for c in chaine.lower():
        if c in "aeiou":
            compte[0] += 1
        elif c in string.ascii_lowercase:
            compte[1] += 1
    return compte

random et fonction impure #

Une fonction pure est une fonction sans état, qui renvoie le même résultat si on lui donne les mêmes paramètres, et qui n’a pas d’effet de bord.

La fonction random.random() renvoie un nombre aléatoire entre 0 et 1. Bien sûr c’est une fonction impure, avec un état (sinon, on aurait toujours le même nombre aléatoire, ce qui n’est pas très utile).

Le code suivante fait des tirages aléatoires, avec random et comptabilise le nombre de tirages dans les intervalles $\left[0, \frac{1}{3}\right[$, $\left[\frac{1}{3},\frac{2}{3}\right[$, $\left[\frac{2}{3}, 1\right[$.

Le tirage est censé être uniforme et pourtant… :

import random

N = 100000
tiers_1, tiers_2, tiers_3 = 0, 0, 0
for k in range(N):
    if random.random() < 1/3:
        tiers_1 += 1
    elif 1/3 <= random.random() < 2/3:
        tiers_2 += 1
    else:
        tiers_3 += 1

print(tiers_1, tiers_2, tiers_3)

affiche :

33200 22552 44248

Le nombre de valeurs dans le premier tiers semble correct, mais pas dans les autres.

Une idée du problème ?