Exceptions C : raise — lancer une erreur nous-mêmes

Objectifs de la leçon
  • Lancer nous-mêmes une Exception avec raise et 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 except avec raise seul

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 :

  1. Le faire en silence — le solde augmente de 500. Un grave bug financier.
  2. 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.
  3. Renvoyer une valeur spéciale (comme -1 ou None) — l’appelant doit penser à la vérifier. S’il oublie, le bug « fuit ».
  4. 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ôleQuiCe qu’il fait
Détecte le problèmeLa fonction qui reçoit les donnéesVérifie et lance (raise)
Décide quoi faireCelui qui a appelé la fonctionAttrape (except) et décide : un message, une nouvelle tentative, une valeur par défaut
Analogie : la réserve d’un restaurant

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 :

TypeQuand
ValueErrorLe type est bon, mais la valeur est invalide : un âge négatif, un montant nul, un e-mail sans @
TypeErrorLe type est faux : on a reçu du texte au lieu d’un nombre
KeyError / LookupErrorOn cherche quelque chose qui n’existe pas
PermissionErrorPas 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...")
    raise

Premier 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 -500

Analyse du code ligne par ligne

LigneExplication
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 - amountOn 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: 500

Analyse :

  • withdraw n’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 withdraw pourra 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 HTTP 400…).

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")
    raise

raise 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 - amount

La 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 150
Afficher 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 150

La 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 allowed
Afficher 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 allowed

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

  1. report_price(price) — affiche le prix après une remise de 10 %, ou invalid price s’il y a eu une erreur.
  2. 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
0
Afficher 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
0

La 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 - amount

Quelqu’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 balance reçoit None. Le programme continue avec un solde de None, et plantera plus tard ailleurs — loin du vrai problème.
  • B : une ValueError est 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 120
Afficher 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 120

parse_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.
  • ValueError pour une valeur invalide, TypeError pour un type faux. Pas une Exception générale.
  • Un bon message = quelle est la règle + ce qui a été reçu.
  • raise seul dans un except transmet 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.

Documentation officielle