Définition
En Python, une erreur est fatale par défaut. Dès qu'une opération échoue, l'interpréteur interrompt tout, affiche une trace, et le reste du programme ne s'exécute jamais. Ce comportement rend service pendant l'écriture du code, où vous voulez être prévenu au plus vite.
Il devient pénalisant une fois le programme entre les mains de quelqu'un d'autre. Un fichier contient une ligne mal formée sur dix mille, un utilisateur saisit du texte là où un nombre était attendu, un serveur distant met trop longtemps à répondre : ces échecs sont prévisibles, ils ne relèvent pas du bug, et tout arrêter est une réponse disproportionnée.
except répond exactement à ce besoin. Il intercepte une exception levée dans le bloc try qui le précède, annonce le type d'erreur qu'il sait traiter, et son bloc ne s'exécute que si l'erreur survenue correspond à ce type.
Ce que cela change pour vous : l'erreur a bien eu lieu, mais c'est vous qui décidez de ce qu'elle devient. Le programme reprend ensuite son cours, et c'est toute la différence entre un incident et un arrêt. Dans l'exemple qui suit, une saisie illisible ne fait plus tomber le programme, elle déclenche une valeur de repli.
try:
quantite = int(saisie) # échoue si la saisie n'est pas un nombre
except ValueError:
quantite = 1 # on repart sur une valeur sûre
print("Valeur non comprise, quantité fixée à 1")Si la conversion réussit, le bloc except est ignoré : il ne coûte rien au cas normal. Si elle échoue, l'exécution saute directement à la première ligne du except, et les lignes restantes du try sont abandonnées.
Nommer l'erreur, toujours
Python accepte aussi un except sans aucun type, écrit except: tout court. La tentation est compréhensible : une seule ligne, et plus rien ne plante jamais.
Le problème est qu'il attrape absolument tout. Les erreurs que vous aviez anticipées, mais aussi l'interruption clavier, et surtout vos propres fautes de programmation.
# Catastrophique : une faute de frappe devient invisible
try:
total = prix * qauntite # « qauntite » n'existe nulle part
except:
total = 0 # avalé sans un mot
# Ciblé : seul ce qui était prévu est rattrapé
try:
total = prix * quantite
except TypeError:
total = 0Dans le premier cas, la NameError provoquée par la faute de frappe est avalée au même titre qu'une erreur légitime. Le total vaut zéro, les documents partent avec un montant faux, et rien dans les journaux ne dira pourquoi. Le programme ne plante pas : il ment, ce qui coûte infiniment plus cher à diagnostiquer qu'un arrêt franc.
Nommer le type sépare deux intentions très différentes. D'un côté, savoir qu'une opération précise peut échouer et prévoir la suite. De l'autre, ne plus vouloir voir d'erreur du tout. Seule la première est un traitement d'erreur.
Plusieurs blocs, un ordre qui compte
Un même try peut échouer de plusieurs façons, et chaque cause mérite souvent sa propre réponse. On enchaîne alors plusieurs except à la suite. Ils sont lus de haut en bas : le premier dont le type correspond gagne, son bloc s'exécute, les autres sont ignorés.
Reste à savoir ce que correspondre veut dire au juste. Les exceptions forment une hiérarchie : KeyError et IndexError descendent toutes deux de LookupError, qui descend elle-même de Exception. Intercepter une classe parente attrape donc aussi toutes ses filles, souvent sans que vous y ayez pensé.
Un cas large placé avant un cas précis rend le second inaccessible : le bloc est bien là, il ne s'exécutera jamais, et Python n'émet aucun avertissement. Allez toujours du plus spécifique au plus général.
Voici les quatre écritures que vous croiserez le plus souvent, classées de la plus ciblée à la plus large.
| Écriture | Ce qu'elle attrape |
|---|---|
except ValueError: | Uniquement ce type |
except (ValueError, TypeError): | Les deux, même traitement |
except LookupError: | KeyError et IndexError ensemble |
except Exception: | Presque tout, à réserver au dernier recours |
Récupérer le détail
Attraper l'erreur suffit pour continuer, pas pour comprendre. Un message écrit à la main dans le except vous apprend qu'une ligne a échoué, jamais laquelle ni pourquoi, et c'est précisément ce qui manque le jour où le problème remonte.
Le mot-clé as règle ce point. Il met l'objet d'exception à disposition sous un nom, et cet objet porte le détail exact, valeur fautive comprise.
except ValueError as erreur:
journaliser(f"Ligne {numero} ignorée : {erreur}")Vient alors la question suivante : que faire quand le traitement local ne suffit pas, quand vous voulez garder une trace du passage sans prétendre avoir réglé le problème ? Un except peut relancer ce qu'il a attrapé avec un raise nu, une fois l'erreur journalisée. Elle poursuit sa remontée vers l'appelant, et la trace d'origine est conservée intacte.
Un except ne protège que le try auquel il est rattaché. Une erreur levée à l'intérieur du bloc except lui-même n'est rattrapée par personne et remonte normalement.
Questions fréquentes
Faut-il rattraper Exception en dernier recours ?
Uniquement à la frontière d'un programme, par exemple autour d'une tâche de fond qui ne doit jamais s'arrêter, ou d'une requête web qui doit répondre quelque chose plutôt que rien. Et à une condition : journaliser l'erreur complète, trace comprise. Un except qui attrape tout sans rien écrire est indistinguable d'un bug.
Que se passe-t-il si aucun except ne correspond ?
L'exception poursuit sa remontée comme si le try n'existait pas, jusqu'à trouver plus haut un bloc capable de la traiter, ou jusqu'à l'arrêt du programme. Le bloc finally, lui, s'exécute quand même au passage, ce qui laisse le temps de fermer proprement un fichier ou une connexion.
Un except peut-il contenir un return ?
Oui, c'est même une écriture courante pour renvoyer une valeur de repli. Veillez simplement à ce que l'appelant puisse distinguer ce repli d'un résultat normal : renvoyer zéro quand le calcul a échoué et quand il vaut réellement zéro rend l'erreur invisible. Un None explicite lève l'ambiguïté.