Définition
Imaginez une fonction qui divise deux nombres. Le calcul est trivial, sauf quand le diviseur vaut zéro. Continuer produirait alors un plantage confus plus loin, une fois ce résultat impossible réutilisé ailleurs. La bonne réponse est de s'arrêter tout de suite, à l'endroit où les choses tournent mal, et de le dire clairement.
raise sert exactement à ça : l'instruction déclenche volontairement une exception, l'exécution s'arrête à cette ligne, et l'erreur remonte la pile des appels comme si Python l'avait levée elle-même. C'est ce qui donne au code le droit de refuser de continuer.
Voici cette idée appliquée à la division :
def diviser(a, b):
if b == 0:
# On s'arrête ici, pas trois lignes plus loin
raise ValueError("Le diviseur ne peut pas être nul")
return a / bPourquoi lever plutôt que renvoyer une valeur
Face à une situation qu'elle ne peut pas traiter, une fonction a deux chemins possibles : renvoyer une valeur signalant l'échec, un -1 ou un None que l'appelant est censé vérifier, ou lever une exception. Ces deux chemins ne se valent pas.
Le problème du premier chemin est qu'il repose sur la discipline de l'appelant. Rien ne l'oblige à vérifier la valeur de retour, et dans la pratique, il ne le fait pas toujours. Comparez ces deux versions d'une même idée :
# L'appelant peut ignorer le -1, et le fera
def trouver_client(identifiant):
...
return -1
# L'appelant ne peut pas ignorer ceci
def trouver_client(identifiant):
...
raise LookupError(f"Client {identifiant} introuvable")La valeur de repli produit des bugs qui se manifestent loin de leur cause : le -1 renvoyé pour signaler un échec finit par servir d'identifiant valide, et l'erreur n'apparaît que bien plus tard, sur une tout autre donnée. L'exception s'arrête à la source et nomme le problème tout de suite.
Choisir le bon type
Lever une exception ne suffit pas : encore faut-il choisir le bon type, ce qui n'est pas cosmétique. C'est lui qui permet à l'appelant de savoir, sans même lire le message d'erreur, à quelle catégorie de problème il a affaire.
Python fournit déjà une famille d'exceptions pour les situations les plus courantes, résumées ici :
| Situation | Type attendu |
|---|---|
| Le bon type, une valeur inacceptable | ValueError |
| Un type qui ne convient pas | TypeError |
| Une opération interdite dans cet état | RuntimeError |
| Une fonctionnalité pas encore écrite | NotImplementedError |
| Un cas métier propre au projet | Une classe d'exception maison |
Aucun de ces types ne convient vraiment à un cas propre au projet, et c'est très bien ainsi : une classe qui hérite d'Exception suffit à créer une exception maison, rattrapable spécifiquement sans risquer d'attraper au passage une erreur du langage.
Lever un Exception nu, sans type précis, est tentant parce que ça marche toujours. Le prix se paie plus tard : personne ne peut la rattraper spécifiquement sans rattraper aussi toutes les autres, fautes de programmation comprises.
Relancer sans effacer la trace
Un cas revient souvent : un bloc except attrape une erreur uniquement pour la journaliser, sans vouloir la traiter lui-même. La question qui se pose alors est de savoir comment la laisser repartir sans perdre l'information qu'elle transportait.
Un raise nu, à l'intérieur d'un bloc except, répond exactement à ce besoin : il relance l'exception en cours en conservant sa trace d'origine. Voici la forme à retenir :
except ConnectionError:
journaliser("Service injoignable")
raise # la trace d'origine est préservéeRelever une nouvelle exception à la place de ce raise nu efface le contexte d'origine, sauf à la chaîner explicitement avec raise X from erreur, qui affiche les deux dans le traceback.
Il existe une variante volontaire : raise X from None. Elle masque la cause d'origine, un choix parfois assumé dans une bibliothèque publique, quand l'erreur interne n'apporterait rien à quelqu'un qui ne lit pas le code source.
Valider aux frontières, pas partout
Une fois qu'on sait lever des exceptions, la tentation est de vérifier les mêmes données à chaque étage du programme, par prudence. Le résultat est un code alourdi de vérifications qui se répètent sans que la fiabilité y gagne réellement.
La règle qui fonctionne est plus simple : valider une fois, là où la donnée entre dans le programme, qu'il s'agisse d'un fichier, d'une réponse d'interface distante ou d'une saisie utilisateur. Passé ce point, le reste du code peut faire confiance à ce qu'il reçoit. Exemple sur la lecture d'une commande :
def charger_commande(donnees):
if "quantite" not in donnees:
raise ValueError("Champ quantite absent")
if donnees["quantite"] <= 0:
raise ValueError(f"Quantité invalide : {donnees['quantite']}")
return Commande(**donnees)Multiplier les vérifications en profondeur n'apporte rien de plus. Si une donnée fautive a déjà traversé la moitié du programme avant d'être détectée, c'est que la frontière n'était pas au bon endroit.
Questions fréquentes
Quelle différence avec assert ?
assert vérifie une hypothèse pendant le développement et disparaît quand Python est lancé en mode optimisé : ce n'est pas un outil de validation fiable. raise reste actif en toutes circonstances, ce qui en fait le bon choix pour valider une donnée venue de l'extérieur du programme.
Faut-il un message dans l'exception ?
Toujours, et le plus précis possible. Un message comme « Valeur invalide » n'aide personne au moment de déboguer, alors que « quantité négative reçue : -3 » désigne directement la donnée fautive et fait gagner un aller-retour de diagnostic.
Peut-on lever une exception dans un except ?
Oui, et c'est même courant : c'est la façon normale de traduire une erreur technique en erreur métier, compréhensible par le reste du programme. Pensez à chaîner avec from pour que la cause d'origine reste visible dans la trace.