# Exceptions B : else et finally — la structure complète<no value>

## Qu'allons-nous apprendre ?

Dans la leçon précédente, nous avons utilisé `try` et `except`. La structure comporte deux autres parties :

- **`else`** — s'exécute **seulement s'il n'y a pas eu d'erreur**.
- **`finally`** — s'exécute **toujours**, avec ou sans erreur.

## Pourquoi en avons-nous besoin ?

Imaginez un code qui ouvre une connexion à une base de données (les thèmes backend et bases de données), effectue une action et ferme la connexion. Que se passe-t-il si l'action échoue au milieu ? Si la fermeture est écrite après l'action, elle **ne sera pas exécutée**, et la connexion reste ouverte. Après une centaine d'échecs de ce genre, le serveur plantera faute de connexions disponibles.

`finally` garantit que le nettoyage a lieu **dans tous les cas**. `else` garde le `try` petit, et rend clair ce qui ne s'exécute qu'après une réussite.

## Explication à partir des bases

### Les quatre blocs

| Bloc | Quand il s'exécute | À quoi il sert |
|---|---|---|
| `try` | On commence toujours par lui | Le code qui **peut échouer** |
| `except` | **Seulement si** une erreur du type correspondant a été lancée | Gérer l'erreur |
| `else` | **Seulement si aucune** erreur n'a été lancée dans le `try` | La suite qui dépend de la réussite |
| `finally` | **Toujours**, à la fin, dans tous les cas | Le nettoyage : fermer des fichiers, des connexions, un message de fin |

{{< box "analogy" "Analogie : la cuisine" >}}
**try** = vous essayez de faire un gâteau. **except** = s'il a brûlé, vous en commandez un à la boulangerie. **else** = s'il est réussi, vous le décorez (on ne décore pas un gâteau brûlé !). **finally** = dans tous les cas, vous éteignez le four et faites la vaisselle.
{{< /box >}}

### Pourquoi `else` et pas simplement continuer dans le `try` ?

On peut mettre le code de la suite à l'intérieur du `try`. Mais alors, si **ce** code échoue avec la même erreur, le `except` l'attrapera par erreur, et traitera le mauvais problème. `else` rend les choses claires : **seulement la ligne risquée dans le `try`**, et le reste à l'extérieur.

### `finally` s'exécute même quand...

- Il y a eu une erreur et elle a été attrapée.
- Il y a eu une erreur et elle n'a **pas** été attrapée. `finally` s'exécute, puis le programme plante.
- Il y a un `return` au milieu du `try`. `finally` s'exécute **avant** que la fonction ne renvoie vraiment.

## Syntaxe

```python
try:
    # risky code
except SomeError:
    # runs only if SomeError happened
else:
    # runs only if NO error happened in try
finally:
    # runs ALWAYS, last
```

**Ordre obligatoire :** `try` → `except` (un ou plusieurs) → `else` (facultatif) → `finally` (facultatif). On peut aussi avoir `try` + `finally` sans `except`.

## Premier exemple de code

`week-2/full_try.py` :

```python
def convert(text):
    print("--- converting:", text)
    try:
        number = int(text)
    except ValueError:
        print("except: not a number")
    else:
        print("else: success, doubled =", number * 2)
    finally:
        print("finally: done with", text)

convert("21")
convert("hi")
```

**Sortie :**

```text
--- converting: 21
else: success, doubled = 42
finally: done with 21
--- converting: hi
except: not a number
finally: done with hi
```

## Analyse du code ligne par ligne

| Bloc | `convert("21")` | `convert("hi")` |
|---|---|---|
| `try: number = int(text)` | Réussit : `number = 21` | `ValueError` |
| `except ValueError:` | Sauté | **S'exécute** |
| `else:` | **S'exécute** : affiche 42 | Sauté |
| `finally:` | **S'exécute** | **S'exécute** |

`else` et `except` ne s'exécutent **jamais tous les deux** : soit il y a eu une erreur, soit il n'y en a pas eu. `finally`, en revanche, s'exécute **dans les deux cas**.

## Un autre exemple

**Une « connexion » qui doit être fermée.** Ici, nous simulons une connexion à une ressource externe (comme une base de données), et voyons que `finally` la ferme aussi bien quand quelque chose échoue au milieu que lorsqu'il y a un `return`. `week-2/connection_demo.py` :

```python
def open_connection():
    print("[connection opened]")

def close_connection():
    print("[connection closed]")

def fetch_price(product_name, prices):
    open_connection()
    try:
        price = prices[product_name]
    except KeyError:
        print("No such product:", product_name)
        return None
    else:
        print("Found", product_name)
        return price
    finally:
        close_connection()

catalog = {"bread": 8, "milk": 6}

print("Result:", fetch_price("milk", catalog))
print()
print("Result:", fetch_price("cake", catalog))
```

**Sortie :**

```text
[connection opened]
Found milk
[connection closed]
Result: 6

[connection opened]
No such product: cake
[connection closed]
Result: None
```

**Analyse :**

- `catalog = {"bread": 8, "milk": 6}` — c'est un **dict** (dictionnaire) : chaque clé (`"bread"`) est associée à une valeur (`8`). `prices["milk"]` renvoie 6. Nous l'apprendrons en détail dans le thème structures de données et fonctions. Pour l'instant, il suffit de savoir qu'accéder à une clé qui n'existe pas lance une `KeyError`.
- Remarquez : il y a un `return` à la fois dans `except` et dans `else`. **Et pourtant** `[connection closed]` est affiché avant le `Result`. Autrement dit, `finally` s'exécute **après** que le `return` a été « décidé », mais **avant** que la fonction ne renvoie vraiment.
- Sans `finally`, il faudrait écrire `close_connection()` avant **chaque** `return`. Facile à oublier, et beaucoup de duplication.

## Erreur courante

**Erreur 1 : mettre le nettoyage après le `try`/`except` au lieu de dans `finally`.**

```python
open_connection()
try:
    price = prices[name]
except KeyError:
    print("missing")
    return None
close_connection()   # never reached when except returns!
```

En cas de `KeyError`, le `return` quitte la fonction, et `close_connection()` **ne s'exécute pas**. La connexion reste ouverte. `finally` résout exactement ce problème.

**Erreur 2 : le code de la suite dans le `try`, et un `except` qui attrape la mauvaise erreur.**

```python
try:
    age = int(text)
    category = ["child", "adult"][age // 18]   # can also raise IndexError... or ValueError?
except ValueError:
    print("Age must be a number")
```

Si la ligne de la suite échoue, par exemple avec une `IndexError` pour l'âge 40 (40 // 18 = 2, et il n'y a pas d'indice 2), le message n'a aucun rapport. Ne mettez que `int(text)` dans le `try`, et la suite dans `else`.

**Erreur 3 : croire que `finally` « attrape » l'erreur.** Ce n'est pas le cas. S'il n'y a pas de `except` correspondant, `finally` s'exécute, et **ensuite** le programme plante avec un Traceback.

<!-- 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: Prédire la sortie

Écrivez ce qui sera affiché pour chaque appel — avant de l'exécuter :

```python
def half(text):
    try:
        number = int(text)
    except ValueError:
        print("not a number:", text)
    else:
        print("half is", number / 2)
    finally:
        print("checked", text)

half("8")
half("x")
```

{{< solution >}}
```python
def half(text):
    try:
        number = int(text)
    except ValueError:
        print("not a number:", text)
    else:
        print("half is", number / 2)
    finally:
        print("checked", text)


half("8")
half("x")
```

```text
half is 4.0
checked 8
not a number: x
checked x
```

- Pour `"8"`, il n'y a pas d'erreur, donc `else` s'exécute, suivi de `finally`.
- Pour `"x"`, une `ValueError` est lancée, donc `except` s'exécute — et `else` ne s'exécute **pas**. `finally` s'exécute dans les deux cas.
{{< /solution >}}

### Exercice 2: finally s'exécute même après return

```python
def check(number):
    try:
        if number > 0:
            return "positive"
        return "not positive"
    finally:
        print("done checking", number)

print(check(5))
print(check(-2))
```

Qu'est-ce qui sera affiché, et dans quel ordre ? Expliquez pourquoi.

{{< solution >}}
```python
def check(number):
    try:
        if number > 0:
            return "positive"
        return "not positive"
    finally:
        print("done checking", number)


print(check(5))
print(check(-2))
```

```text
done checking 5
positive
done checking -2
not positive
```

`finally` s'exécute **avant** que la fonction ne renvoie vraiment — même quand il y a un `return` dans le `try`. C'est pourquoi « done checking » est affiché avant la valeur renvoyée par la fonction.
{{< /solution >}}

### Exercice 3: Les quatre blocs

Écrivez une fonction `parse_quantity(text)` qui utilise les quatre blocs :

- `try` — convertit le texte en nombre ;
- `except` — affiche `invalid: <text>` ;
- `else` — affiche `quantity: <number>` ;
- `finally` — affiche `---`.

```python
parse_quantity("3")
parse_quantity("three")
```

**Résultat attendu :**

```text
quantity: 3
---
invalid: three
---
```

{{< solution >}}
```python
def parse_quantity(text):
    try:
        quantity = int(text)
    except ValueError:
        print("invalid:", text)
    else:
        print("quantity:", quantity)
    finally:
        print("---")


parse_quantity("3")
parse_quantity("three")
```

```text
quantity: 3
---
invalid: three
---
```

Le code qui ne s'exécute que lorsque tout a réussi est dans `else` et non dans le `try` — ainsi le `try` reste petit, et n'attrape que la ligne qui peut vraiment échouer.
{{< /solution >}}

### Exercice 4: Fermer ce que l'on a ouvert

Les fonctions `open_register()` et `close_register()` ouvrent et ferment une caisse enregistreuse. Écrivez une fonction `sell(prices, position)` qui ouvre la caisse, renvoie le prix à la position choisie (la position est un indice de la liste — elle commence à 0), et ferme **toujours** la caisse — même quand la position n'existe pas. Une position qui n'existe pas renvoie `None`.

```python
def open_register():
    print("register opened")

def close_register():
    print("register closed")

print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))
```

**Résultat attendu :**

```text
register opened
register closed
20
register opened
register closed
None
```

{{< solution >}}
```python
def open_register():
    print("register opened")


def close_register():
    print("register closed")


def sell(prices, position):
    open_register()
    try:
        return prices[position]
    except IndexError:
        return None
    finally:
        close_register()


print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))
```

```text
register opened
register closed
20
register opened
register closed
None
```

C'est exactement le rôle de `finally` : le **nettoyage** — fermer une ressource qui a été ouverte, quelle que soit la façon dont la fonction s'est terminée.
{{< /solution >}}

### Exercice 5: else ou dans le try ?

Deux développeurs ont écrit le même code de deux façons :

```text
# A                              # B
try:                             try:
    number = int(text)               number = int(text)
    print(100 / number)          except ValueError:
except ValueError:                   print("not a number")
    print("not a number")        else:
                                     print(100 / number)
```

Quelle est la différence, et quelle version est la meilleure ? Que se passera-t-il dans chacune quand `text` vaut `"0"` ?

{{< solution >}}
- Dans **A**, la division est aussi dans le `try`. Dans **B**, seule la conversion est dans le `try`, et la division est passée dans `else`.
- Quand `text` vaut `"0"`, les deux versions lancent une `ZeroDivisionError`, qu'aucun `except` n'attrape, et le programme plante. Mais dans **B**, il est tout à fait clair que le `except ValueError` ne concerne **que** la conversion.
- **B est meilleure** : un `try` aussi petit que possible. Ainsi, `except` n'attrape pas par erreur des erreurs venant d'autres lignes, et il est facile de voir ce que fait chaque partie.
{{< /solution >}}

<!-- exercises:end -->

## Résumé

- **`try`** — le code risqué. **`except`** — seulement si une erreur correspondante s'est produite. **`else`** — seulement s'il n'y a **pas** eu d'erreur. **`finally`** — **toujours**.
- `except` et `else` ne s'exécutent jamais ensemble.
- `finally` s'exécute même après un `return`, et aussi quand l'erreur n'a pas été attrapée (et le programme plante ensuite).
- **`else`** garde le `try` petit et ciblé. **`finally`** est l'endroit du nettoyage : fermer les fichiers et les connexions.

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

## Documentation officielle

- [Python Tutorial — Defining Clean-up Actions](https://docs.python.org/3/tutorial/errors.html#defining-clean-up-actions)
- [The try statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement)

<!-- docs:end -->
