Exceptions C : raise — lancer une erreur nous-mêmes
- Lancer nous-mêmes une Exception avec
raiseet un message clair - Choisir un type d’erreur adapté — ValueError, TypeError et autres
- Comprendre la répartition du travail : la fonction détecte un problème et lance, l’appelant décide comment le gérer
- Transmettre une erreur depuis un
exceptavecraiseseul
Qu’allons-nous apprendre ?
Jusqu’à présent, c’est Python qui lançait les erreurs, et nous les attrapions. Aujourd’hui, nous allons apprendre à les lancer nous-mêmes, avec raise, quand notre code détecte un état invalide.
Pourquoi en avons-nous besoin ?
Supposons que vous ayez écrit une fonction withdraw(balance, amount) pour retirer de l’argent d’un compte bancaire. Que doit-elle faire quand on lui demande de retirer moins 500 ? Les options :
- Le faire en silence — le solde augmente de 500. Un grave bug financier.
- Afficher « erreur » — mais celui qui a appelé la fonction ne sait pas qu’elle a échoué, et continue de travailler avec un résultat faux.
- Renvoyer une valeur spéciale (comme
-1ouNone) — l’appelant doit penser à la vérifier. S’il oublie, le bug « fuit ». - Lancer une erreur — l’appelant doit s’en occuper, sinon le programme s’arrête avec un message clair exactement à l’endroit problématique.
L’option 4 est la façon professionnelle. Dans le thème POO, nous l’utiliserons pour la validation à l’intérieur des classes, et dans les thèmes backend et bases de données, un serveur FastAPI transformera des erreurs de ce genre en une réponse HTTP 400 ou 404.
Explication à partir des bases
raise
raise lance une Exception, exactement comme le fait Python quand quelque chose casse. À partir du raise :
- La fonction s’arrête immédiatement. Aucune ligne après lui ne s’exécute, et aucune valeur n’est renvoyée.
- L’erreur « remonte » vers celui qui a appelé la fonction. S’il l’attrape avec
except, parfait. Sinon, elle continue de remonter, et à la fin le programme plante avec un Traceback.
La bonne répartition : qui détecte et qui gère
| Rôle | Qui | Ce qu’il fait |
|---|---|---|
| Détecte le problème | La fonction qui reçoit les données | Vérifie et lance (raise) |
| Décide quoi faire | Celui qui a appelé la fonction | Attrape (except) et décide : un message, une nouvelle tentative, une valeur par défaut |
Le serveur (l’appelant) demande au magasinier (la fonction) 5 tomates. Il n’y a pas de tomates. Un bon magasinier n’apporte pas en silence 5 pommes (une valeur fausse), et ne « décide » pas tout seul de changer le plat. Il annonce (raise) : « Plus de tomates ! ». Et le serveur, qui connaît le client, décide : proposer un autre plat ou s’excuser. Chacun est responsable de son domaine.
Quel type d’erreur lancer ?
Utilisez les types intégrés, selon leur sens :
| Type | Quand |
|---|---|
ValueError | Le type est bon, mais la valeur est invalide : un âge négatif, un montant nul, un e-mail sans @ |
TypeError | Le type est faux : on a reçu du texte au lieu d’un nombre |
KeyError / LookupError | On cherche quelque chose qui n’existe pas |
PermissionError | Pas de permission pour l’action |
Le message doit expliquer ce qui ne va pas et pourquoi : "amount must be positive, got -500", et non "error".
(Dans le thème POO, après avoir appris les classes, nous verrons comment créer nos propres types d’erreurs, comme InsufficientFundsError.)
Syntaxe
raise ValueError("clear message about what is wrong")
# typical pattern: validate at the start of a function
def function_name(value):
if not is_valid(value):
raise ValueError("why it is invalid")
# ... normal work, only with valid data
# the caller decides
try:
function_name(bad_value)
except ValueError as error:
print("Problem:", error)
# re-raise: handle partly, then pass the same error up
except ValueError:
print("logging the problem...")
raisePremier exemple de code
week-2/withdraw.py :
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("amount must be positive, got " + str(amount))
if amount > balance:
raise ValueError("insufficient funds: balance " + str(balance) + ", requested " + str(amount))
return balance - amount
print(withdraw(1000, 300))
print(withdraw(1000, -500))
print("this line never runs")Sortie :
700
Traceback (most recent call last):
File "withdraw.py", line 10, in <module>
print(withdraw(1000, -500))
File "withdraw.py", line 3, in withdraw
raise ValueError("amount must be positive, got " + str(amount))
ValueError: amount must be positive, got -500Analyse du code ligne par ligne
| Ligne | Explication |
|---|---|
if amount <= 0: | Validation (vérification de l’entrée) : on vérifie avant de faire quoi que ce soit. |
raise ValueError("...") | Crée un objet ValueError avec un message, et le lance. La fonction s’arrête ici. |
if amount > balance: | Une deuxième vérification, pour une autre règle métier. |
return balance - amount | On n’arrive ici que si toutes les vérifications sont passées. À partir d’ici, les données sont sûres. |
withdraw(1000, 300) | Passe les vérifications et renvoie 700. |
withdraw(1000, -500) | Une erreur est lancée. Personne ne l’attrape, donc le programme plante avec un Traceback clair. La dernière ligne de code ne s’exécute pas. |
Remarquez la qualité du Traceback : le message dit exactement quel est le problème (amount must be positive) et quelle valeur l’a causé (-500). Voilà à quoi ressemble une bonne erreur.
Un autre exemple
L’appelant décide. La même fonction, avec du code qui attrape et gère. week-2/atm_safe.py :
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("amount must be positive, got " + str(amount))
if amount > balance:
raise ValueError("insufficient funds: balance " + str(balance) + ", requested " + str(amount))
return balance - amount
balance = 1000
requests = [200, -50, 5000, 300]
for amount in requests:
try:
balance = withdraw(balance, amount)
except ValueError as error:
print("Rejected:", error)
else:
print("Withdrew", amount, "- new balance:", balance)
print("Final balance:", balance)Sortie :
Withdrew 200 - new balance: 800
Rejected: amount must be positive, got -50
Rejected: insufficient funds: balance 800, requested 5000
Withdrew 300 - new balance: 500
Final balance: 500Analyse :
withdrawn’affiche rien et ne sait pas ce qui va se passer ensuite. Elle se contente de détecter et de lancer.- La boucle (l’appelant) décide : la demande est refusée avec un message, et la boucle passe à la demande suivante.
- Quand l’erreur est lancée, l’affectation
balance = withdraw(...)n’est pas exécutée, le solde reste donc tel quel. C’est exactement ce que nous voulions. - Demain, la même
withdrawpourra servir sur un site web, dans une application ou dans un serveur, et chacun gérera l’erreur à sa façon (un message à l’écran, une réponse HTTP400…).
raise sans rien après — transmettre
Parfois, on veut faire quelque chose quand il y a une erreur (par exemple, l’enregistrer), mais sans la « faire disparaître » :
try:
balance = withdraw(balance, 5000)
except ValueError:
print("Audit: a failed withdrawal attempt was recorded")
raiseraise seul, dans un except, relance exactement la même erreur. Elle continue de remonter vers celui qui est au-dessus.
Erreur courante
Erreur 1 : print au lieu de raise.
def withdraw(balance, amount):
if amount > balance:
print("Not enough money!")
return balance - amountLa fonction affiche un message, mais continue et renvoie un solde négatif ! L’appelant ne sait pas que quelque chose s’est mal passé. L’affichage est aussi « coincé » à l’écran, et ne signifie rien dans un serveur ou une application.
Erreur 2 : un message inutile.
raise ValueError("error")
raise ValueError("bad input")Quelle entrée ? Quelle était la valeur ? Qu’est-ce qui est attendu ? Un bon message = quelle est la condition + ce qui a été reçu : "age must be between 0 and 120, got 150".
Erreur 3 : lancer le type le plus général.
raise Exception("amount must be positive")Exception est le parent de presque toutes les erreurs. Celui qui veut attraper uniquement ce problème aura besoin de except Exception, qui attrape aussi des bugs sans rapport. Choisissez un type précis (ValueError).
Erreur 4 : attraper et relancer, en perdant le contexte.
except ValueError:
raise ValueError("something failed")Le message d’origine (avec les détails) a été perdu. Si vous voulez le transmettre, raise seul conserve l’erreur d’origine.
Exercices
Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.
Exercice 1: Un âge valide
Écrivez une fonction set_age(age) qui renvoie l’âge s’il est compris entre 0 et 120, et sinon lance une ValueError avec le message indiqué dans le résultat attendu (y compris l’âge reçu). Écrivez ensuite une boucle qui essaie les âges [25, -3, 150] : pour un âge valide, elle affiche ok: et l’âge, et pour une erreur — error: et le message.
Résultat attendu :
ok: 25
error: age must be between 0 and 120, got -3
error: age must be between 0 and 120, got 150Afficher la solution
MIN_AGE = 0
MAX_AGE = 120
def set_age(age):
if age < MIN_AGE or age > MAX_AGE:
raise ValueError("age must be between 0 and 120, got " + str(age))
return age
for age in [25, -3, 150]:
try:
print("ok:", set_age(age))
except ValueError as error:
print("error:", error)ok: 25
error: age must be between 0 and 120, got -3
error: age must be between 0 and 120, got 150La fonction ne fait que détecter le problème et lancer. Son appelant — la boucle — décide quoi faire.
Exercice 2: Un nom d’utilisateur
Écrivez une fonction validate_username(name) qui lance une ValueError :
- si le nom fait moins de 3 caractères — avec le message
too short; - s’il contient un espace — avec le message
no spaces allowed.
Si le nom est valide — elle le renvoie.
for name in ["dana", "al", "big boss"]:
try:
print("valid:", validate_username(name))
except ValueError as error:
print(name, "->", error)Résultat attendu :
valid: dana
al -> too short
big boss -> no spaces allowedAfficher la solution
MIN_USERNAME_LENGTH = 3
def validate_username(name):
if len(name) < MIN_USERNAME_LENGTH:
raise ValueError("too short")
if " " in name:
raise ValueError("no spaces allowed")
return name
for name in ["dana", "al", "big boss"]:
try:
print("valid:", validate_username(name))
except ValueError as error:
print(name, "->", error)valid: dana
al -> too short
big boss -> no spaces allowedExercice 3: L’appelant décide
La fonction suivante lance une erreur pour un prix négatif :
def apply_discount(price, percent):
if price < 0:
raise ValueError("price cannot be negative")
return price - price * percent / 100Écrivez deux fonctions qui l’utilisent de deux façons différentes :
report_price(price)— affiche le prix après une remise de 10 %, ouinvalid prices’il y a eu une erreur.price_or_zero(price)— renvoie le prix après une remise de 10 %, ou 0 s’il y a eu une erreur.
report_price(200)
report_price(-5)
print(price_or_zero(50))
print(price_or_zero(-5))Résultat attendu :
180.0
invalid price
45.0
0Afficher la solution
def apply_discount(price, percent):
if price < 0:
raise ValueError("price cannot be negative")
return price - price * percent / 100
def report_price(price):
try:
print(apply_discount(price, 10))
except ValueError:
print("invalid price")
def price_or_zero(price):
try:
return apply_discount(price, 10)
except ValueError:
return 0
report_price(200)
report_price(-5)
print(price_or_zero(50))
print(price_or_zero(-5))180.0
invalid price
45.0
0La même fonction, deux façons différentes de la gérer. C’est pourquoi apply_discount lance et n’affiche rien elle-même : elle ne sait pas ce dont l’appelant a besoin.
Exercice 4: return None face à raise
Deux versions de la même fonction :
# A # B
def withdraw(balance, amount): def withdraw(balance, amount):
if amount > balance: if amount > balance:
print("Not enough money!") raise ValueError("not enough money")
return None return balance - amount
return balance - amountQuelqu’un a écrit : balance = withdraw(100, 500). Que se passera-t-il dans chaque version, et pourquoi B est-elle meilleure ?
Afficher la solution
- A : « Not enough money! » est affiché, et
balancereçoitNone. Le programme continue avec un solde deNone, et plantera plus tard ailleurs — loin du vrai problème. - B : une
ValueErrorest lancée exactement à l’endroit du problème. Si l’appelant ne l’a pas gérée, le Traceback pointe directement vers le retrait problématique. S’il l’a gérée — il décide lui-même quoi afficher. - B n’affiche rien elle-même non plus : peut-être que l’appelant est un serveur qui doit renvoyer un message, et non afficher à l’écran.
Exercice 5: Convertir une note
Écrivez une fonction parse_grade(text) qui convertit un texte en note. Elle lance une ValueError si le texte n’est pas un nombre (cela se produit déjà dans int), ou si la note est en dehors de l’intervalle 0 à 100 — avec le message indiqué dans le résultat attendu (y compris la note reçue).
for text in ["87", "abc", "120"]:
try:
print("grade:", parse_grade(text))
except ValueError as error:
print("rejected:", error)Résultat attendu :
grade: 87
rejected: invalid literal for int() with base 10: 'abc'
rejected: grade must be 0-100, got 120Afficher la solution
def parse_grade(text):
grade = int(text)
if grade < 0 or grade > 100:
raise ValueError("grade must be 0-100, got " + str(grade))
return grade
for text in ["87", "abc", "120"]:
try:
print("grade:", parse_grade(text))
except ValueError as error:
print("rejected:", error)grade: 87
rejected: invalid literal for int() with base 10: 'abc'
rejected: grade must be 0-100, got 120parse_grade n’attrape pas l’erreur de int — elle la laisse passer jusqu’à l’appelant, exactement comme l’erreur qu’elle lance elle-même.
Résumé
raise SomeError("message")lance une erreur. La fonction s’arrête immédiatement, et aucune valeur n’est renvoyée.- La fonction détecte et lance. L’appelant attrape et décide.
ValueErrorpour une valeur invalide,TypeErrorpour un type faux. Pas uneExceptiongénérale.- Un bon message = quelle est la règle + ce qui a été reçu.
raiseseul dans unexcepttransmet la même erreur.- Lancer vaut mieux qu’afficher « erreur » ou renvoyer une valeur spéciale, car on ne peut pas « oublier de vérifier » une erreur.