# Exceptions A : pourquoi gérer les erreurs, try et except<no value>

## Qu'allons-nous apprendre ?

- Ce qu'est une **Exception**.
- Pourquoi un vrai logiciel doit **gérer** les erreurs et ne pas planter.
- La syntaxe `try` / `except`.
- Une gestion **différente** pour différents types d'erreurs.

## Pourquoi en avons-nous besoin ?

Imaginez un petit serveur de calculatrice : un client envoie `"hello"` au lieu d'un nombre, **et tout le serveur plante**. Tous les clients qui viennent ensuite reçoivent « Connection refused ».

Dans le monde réel :

- Un utilisateur tape « abc » dans un champ d'âge. L'application ne doit pas se fermer.
- Un fichier que le programme essaie de lire a été supprimé. Il doit le signaler, pas planter.
- Internet a coupé au milieu d'une requête. Il vaut la peine de réessayer, ou d'afficher un message.

Les erreurs **arriveront** : une mauvaise saisie, des fichiers manquants, un réseau instable. La seule question est ce que fait le logiciel **quand** elles arrivent.

## Explication à partir des bases

### Qu'est-ce qu'une Exception ?

Une **Exception** est un objet que Python **« lance »** (Raise) quand une Runtime Error se produit. Toutes les erreurs que nous avons vues hier (`ValueError`, `ZeroDivisionError`, `TypeError`...) sont des Exceptions.

Quand une Exception est lancée :

1. Python **arrête** immédiatement la ligne en cours.
2. Il cherche quelqu'un qui **« attrapera »** (Catch) l'erreur.
3. Si personne ne l'attrape, **le programme plante** et un Traceback est affiché.

`try` / `except` est le moyen **d'attraper**.

{{< box "analogy" "Analogie : un filet de sécurité au cirque" >}}
Un funambule (votre code) peut tomber (une Exception). Sans filet, la chute est la fin du spectacle (un plantage). **`try`** = « le numéro se fait au-dessus d'un filet ». **`except`** = le filet, qui rattrape la chute, et ensuite vous décidez quoi faire : réessayer, prévenir le public, ou passer au numéro suivant.
{{< /box >}}

### Comment ça marche

- **`try:`** — « essaie d'exécuter ce bloc ».
- **`except SomeError:`** — « si une `SomeError` est lancée dans le `try`, **saute ici** et exécute ce bloc au lieu de planter ».
- Si aucune erreur n'a été lancée, le bloc `except` est **sauté**.
- Après le `try`/`except`, le programme **continue normalement**.

## Syntaxe

```python
try:
    # code that might fail
except ErrorType:
    # what to do if ErrorType happened

# several error types, each handled differently
try:
    ...
except ValueError:
    ...
except ZeroDivisionError:
    ...

# get the error object itself (its message)
except ValueError as error:
    print(error)

# same handling for several types
except (ValueError, TypeError):
    ...
```

## Premier exemple de code

`week-2/safe_age.py` :

```python
text = input("Enter your age: ")

try:
    age = int(text)
    print("Next year you will be", age + 1)
except ValueError:
    print("That is not a whole number:", text)

print("Program continues normally.")
```

**Exécution 1 (saisie valide) :**

```text
Enter your age: 30
Next year you will be 31
Program continues normally.
```

**Exécution 2 (saisie invalide) :**

```text
Enter your age: abc
That is not a whole number: abc
Program continues normally.
```

## Analyse du code ligne par ligne

| Ligne | Exécution 1 (`30`) | Exécution 2 (`abc`) |
|---|---|---|
| `text = input(...)` | `text = "30"` | `text = "abc"` |
| `try:` | Entre dans le bloc | Entre dans le bloc |
| `age = int(text)` | Réussit : `age = 30` | **Une `ValueError` est lancée**, et Python saute directement au `except` |
| `print("Next year...")` | Affiché | **Sauté**. On n'y arrive jamais |
| `except ValueError:` | Sauté (pas d'erreur) | **Attrapée !** |
| `print("That is not...")` | Ne s'exécute pas | Affiché |
| `print("Program continues...")` | Affiché | Affiché, et le programme **n'a pas planté** |

**Le point important :** dès qu'une erreur est lancée, **les lignes restantes du `try` ne s'exécutent pas**. C'est pourquoi on ne met dans le `try` que ce qui dépend de la réussite.

## Un autre exemple

**Une calculatrice de division robuste**, avec une gestion différente pour chaque type d'erreur et une boucle qui redemande. `week-2/safe_divide.py` :

```python
def ask_number(prompt):
    while True:
        text = input(prompt)
        try:
            return float(text)
        except ValueError:
            print("Please type a number, for example 12 or 3.5")

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        print("Cannot divide by zero - returning 0")
        return 0

x = ask_number("First number: ")
y = ask_number("Second number: ")
print("Result:", divide(x, y))
```

**Exemple d'exécution :**

```text
First number: ten
Please type a number, for example 12 or 3.5
First number: 10
Second number: 0
Cannot divide by zero - returning 0
Result: 0
```

**Analyse :**

- `return float(text)` dans un `try` — si la conversion réussit, la fonction **renvoie** immédiatement et sort aussi de la boucle. Si elle échoue, `except` affiche un message, et la boucle redemande. Ainsi, on « oblige » l'utilisateur à une saisie valide.
- Chaque fonction attrape l'erreur **qui la concerne** : `ask_number` attrape `ValueError` (la saisie), `divide` attrape `ZeroDivisionError` (le calcul).
- **Une question ouverte :** `divide` doit-elle renvoyer 0 en cas de division par zéro ? Mathématiquement, c'est une réponse fausse ! Dans bien des cas, il vaut mieux **ne pas** l'attraper ici, et laisser l'appelant décider. Nous en parlerons dans la leçon sur `raise`.

### `as error` — obtenir les détails de l'erreur

```python
try:
    number = int("12.5")
except ValueError as error:
    print("Conversion failed:", error)
```

```text
Conversion failed: invalid literal for int() with base 10: '12.5'
```

`error` est l'objet de l'erreur. L'afficher montre le **message** qui serait apparu à la dernière ligne du Traceback.

## Erreur courante

**Erreur 1 : un `except` vide qui attrape tout.**

```python
try:
    age = int(input("Age: "))
    print("Double:", age * 2)
    reslt = age / 0
except:
    print("Something went wrong")
```

Il y a ici trois problèmes différents (la saisie, la division par zéro, et même une lettre manquante dans un nom de variable), et tous sont cachés sous « Something went wrong ». **Impossible à déboguer.** Et pire encore : un `except:` vide attrape aussi `KeyboardInterrupt`, donc même `Ctrl + C` n'arrêtera pas un programme coincé dans une boucle comme celle-ci !

**La règle :** n'attrapez **que des erreurs précises que vous savez gérer.** Toutes les autres continuent de remonter et font planter, et c'est **une bonne chose**, car c'est ainsi qu'on découvre les bugs.

**Erreur 2 : un `try` énorme.**

```python
try:
    # 40 lines of code...
except ValueError:
    print("Invalid value")
```

Laquelle des 40 lignes a échoué ? Impossible à savoir. **Ne mettez dans le `try` que les lignes qui peuvent échouer avec l'erreur que vous attrapez**, le moins possible.

**Erreur 3 : « avaler » une erreur en silence.**

```python
try:
    price = float(text)
except ValueError:
    pass
```

`pass` signifie « ne rien faire ». L'erreur disparaît, et `price` n'existe peut-être même pas, ce qui provoquera une `NameError` déroutante quelques lignes plus loin. **Faites toujours quelque chose** : un message, une valeur par défaut expliquée, ou une écriture dans un journal.

<!-- 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: Conversion sûre en nombre

Écrivez une fonction `to_number(text)` qui renvoie le texte sous forme de nombre entier, et si le texte n'est pas un nombre — renvoie `None`, sans que le programme plante.

```python
print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))
```

**Résultat attendu :**

```text
42
None
7
```

{{< solution >}}
```python
def to_number(text):
    try:
        return int(text)
    except ValueError:
        return None


print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))
```

```text
42
None
7
```

`int("hello")` lance une `ValueError`. Sans `try`, le programme planterait. Avec `except ValueError`, nous décidons de ce qui se passe à la place.
{{< /solution >}}

### Exercice 2: Une gestion différente pour chaque type d'erreur

Écrivez une fonction `safe_divide(a_text, b_text)` qui reçoit deux textes, les convertit en nombres et divise. Elle renvoie :

- le résultat de la division, si tout est valide ;
- `"numbers only"` si l'un des textes n'est pas un nombre ;
- `"cannot divide by zero"` si le diviseur vaut 0.

```python
print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))
```

**Résultat attendu :**

```text
2.5
cannot divide by zero
numbers only
```

{{< solution >}}
```python
def safe_divide(a_text, b_text):
    try:
        return int(a_text) / int(b_text)
    except ValueError:
        return "numbers only"
    except ZeroDivisionError:
        return "cannot divide by zero"


print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))
```

```text
2.5
cannot divide by zero
numbers only
```

Chaque type d'erreur a son propre `except`, avec une gestion adaptée. Python vérifie les blocs `except` dans l'ordre et entre dans le premier qui correspond.
{{< /solution >}}

### Exercice 3: Afficher le message d'erreur

Le code suivant échoue avec une `ValueError`. Enveloppez-le dans un `try`, attrapez l'erreur avec `except ValueError as error`, et affichez le texte `Could not convert:` suivi du message que Python a enregistré dans `error` :

```python
number = int("12abc")
```

**Résultat attendu :**

```text
Could not convert: invalid literal for int() with base 10: '12abc'
```

{{< solution >}}
```python
try:
    number = int("12abc")
except ValueError as error:
    print("Could not convert:", error)
```

```text
Could not convert: invalid literal for int() with base 10: '12abc'
```

`as error` enregistre l'objet de l'erreur dans une variable. Quand on l'affiche, on obtient le message créé par Python.
{{< /solution >}}

### Exercice 4: Choisir dans une liste

Vous avez une liste de noms. Écrivez une fonction `pick(names, position_text)` qui reçoit une position sous forme de texte (en commençant à 1) et renvoie le nom à cette position. Elle renvoie `"not a number"` pour une saisie qui n'est pas un nombre, et `"no such position"` pour une position qui n'existe pas.

```python
names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))
```

**Résultat attendu :**

```text
Avi
not a number
no such position
```

{{< solution >}}
```python
def pick(names, position_text):
    try:
        position = int(position_text)
        return names[position - 1]
    except ValueError:
        return "not a number"
    except IndexError:
        return "no such position"


names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))
```

```text
Avi
not a number
no such position
```

Deux erreurs différentes à deux endroits : `int(...)` peut lancer une `ValueError`, et l'accès à la liste peut lancer une `IndexError`.
{{< /solution >}}

### Exercice 5: Un except qui attrape tout

```python
def read_age(text):
    try:
        return int(text)
    except:
        return 0
```

1. Pourquoi un `except:` sans type d'erreur est-il dangereux ?
2. Donnez un exemple de vrai bug que ce code cacherait.
3. Corrigez le code.

{{< solution >}}
1. Un `except:` vide attrape **toutes** les erreurs — y compris celles auxquelles nous n'avons pas pensé — et les cache. Le code continue comme si tout allait bien, avec une valeur fausse.
2. Si quelqu'un passe `None` par erreur au lieu d'un texte, une `TypeError` est lancée — un vrai bug dans le code qui a appelé la fonction. Au lieu de le voir, on obtient silencieusement l'âge 0. (Même `Ctrl + C` est attrapé par un `except:` vide.)
3. N'attrapez que l'erreur attendue :

```python
def read_age(text):
    try:
        return int(text)
    except ValueError:
        return 0


print(read_age("30"))
print(read_age("thirty"))
```

```text
30
0
```
{{< /solution >}}

<!-- exercises:end -->

## Résumé

- **Exception** = une erreur que Python lance à l'exécution. Si personne ne l'attrape, le programme plante.
- `try` = « essaie ». `except SomeError` = « si cette erreur s'est produite, fais ceci au lieu de planter ».
- Dès qu'une erreur est lancée, **le reste du `try` est sauté**.
- `except SomeError as error` donne accès au message d'erreur.
- N'attrapez que des **erreurs précises**, avec un `try` **petit**. Pas de `except:` vide ni de `pass` qui avale les erreurs.

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

## Documentation officielle

- [Python Tutorial — Handling Exceptions](https://docs.python.org/3/tutorial/errors.html#handling-exceptions)
- [Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#concrete-exceptions)

<!-- docs:end -->
