raise en Python : déclencher une erreur volontairement

L'instruction raise déclenche volontairement une exception, pour signaler une situation que le code ne doit pas traiter en silence.
5 min de lecture
Believemy logo

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 :

PYTHON
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 / b


Pourquoi 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 :

PYTHON
# 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 :

SituationType attendu
Le bon type, une valeur inacceptableValueError
Un type qui ne convient pasTypeError
Une opération interdite dans cet étatRuntimeError
Une fonctionnalité pas encore écriteNotImplementedError
Un cas métier propre au projetUne 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.

Attention

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 :

PYTHON
except ConnectionError:
    journaliser("Service injoignable")
    raise                     # la trace d'origine est préservée

Relever 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.

Bon à savoir

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 :

PYTHON
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

Question

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.

Question

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.

Question

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.

Termes connexes

Découvrez notre glossaire Python

Parcourez les termes et définitions les plus couramment utilisés dans le domaine du développement avec Python.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.