# Exceptions C : raise — lancer une erreur nous-mêmes<no value>

## 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ô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 |

{{< box "analogy" "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.
{{< /box >}}

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

```python
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` :

```python
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 :**

```text
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

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

```python
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 :**

```text
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 » :

```python
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`.**

```python
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.**

```python
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.**

```python
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.**

```python
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.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## 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 :**

```text
ok: 25
error: age must be between 0 and 120, got -3
error: age must be between 0 and 120, got 150
```

{{< solution >}}
```python
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)
```

```text
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.
{{< /solution >}}

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

```python
for name in ["dana", "al", "big boss"]:
    try:
        print("valid:", validate_username(name))
    except ValueError as error:
        print(name, "->", error)
```

**Résultat attendu :**

```text
valid: dana
al -> too short
big boss -> no spaces allowed
```

{{< solution >}}
```python
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)
```

```text
valid: dana
al -> too short
big boss -> no spaces allowed
```
{{< /solution >}}

### Exercice 3: L'appelant décide

La fonction suivante lance une erreur pour un prix négatif :

```python
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.

```python
report_price(200)
report_price(-5)
print(price_or_zero(50))
print(price_or_zero(-5))
```

**Résultat attendu :**

```text
180.0
invalid price
45.0
0
```

{{< solution >}}
```python
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))
```

```text
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.
{{< /solution >}}

### Exercice 4: return None face à raise

Deux versions de la même fonction :

```text
# 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 ?

{{< 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.
{{< /solution >}}

### 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).

```python
for text in ["87", "abc", "120"]:
    try:
        print("grade:", parse_grade(text))
    except ValueError as error:
        print("rejected:", error)
```

**Résultat attendu :**

```text
grade: 87
rejected: invalid literal for int() with base 10: 'abc'
rejected: grade must be 0-100, got 120
```

{{< solution >}}
```python
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)
```

```text
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.
{{< /solution >}}

<!-- exercises:end -->

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

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Documentation officielle

- [Python Tutorial — Raising Exceptions](https://docs.python.org/3/tutorial/errors.html#raising-exceptions)
- [The raise statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement)

<!-- docs:end -->
