alpinux-wiki/docs/presentations/Python/08-erreurs-et-debogage.md
2026-10-15 00:28:56 +02:00

1694 lines
59 KiB
Markdown

# Chapitre 08 — Erreurs, exceptions et débogage
> **Durée estimée** : 4 à 5 heures (cours + exercices + mini-projet)
>
> **Prérequis** : [Chapitre 07 — Fichiers, CSV et JSON](07-fichiers-csv-json.md) (et tout ce qui précède : variables, conditions, boucles, structures de données, fonctions).
>
> **Objectifs** — À la fin de ce chapitre, tu sauras :
> - lire un *traceback* de bas en haut et trouver en quelques secondes la ligne fautive ;
> - reconnaître les exceptions les plus courantes (`ValueError`, `TypeError`, `KeyError`, `IndexError`, `FileNotFoundError`, `ZeroDivisionError`…) et comprendre ce qu'elles signifient ;
> - intercepter les erreurs avec `try` / `except` / `else` / `finally` sans masquer les vrais bugs ;
> - lever tes propres erreurs avec `raise` et créer tes propres types d'exceptions ;
> - déboguer un programme avec `print`, `breakpoint()` et le débogueur de VS Code (points d'arrêt, pas à pas) ;
> - utiliser `assert` pour vérifier tes hypothèses pendant le développement.
---
## Cours
### 1. Les erreurs ne sont pas tes ennemies
Quand tu débutes, voir un gros pavé rouge s'afficher dans le terminal fait peur. On a l'impression d'avoir « cassé » quelque chose. En réalité, un message d'erreur est **un cadeau** : Python t'explique précisément *ce qui* ne va pas, *où*, et *dans quel contexte*. Les développeurs expérimentés ne font pas moins d'erreurs que toi ; ils savent juste **les lire vite**.
Il existe deux grandes familles d'erreurs :
1. **Les erreurs de syntaxe** (`SyntaxError`, `IndentationError`) : ton code n'est pas du Python valide. Python refuse de le lancer **avant même** d'exécuter la première ligne. C'est comme une lettre écrite dans une langue que le facteur ne comprend pas : il ne la distribue pas.
2. **Les exceptions** : ton code est grammaticalement correct, mais quelque chose se passe mal **pendant l'exécution** (diviser par zéro, ouvrir un fichier qui n'existe pas, convertir `"douze"` en entier…). La lettre est bien écrite, mais l'adresse n'existe pas.
```python
# Erreur de syntaxe : il manque les deux-points
if 3 > 2
print("oui")
```
```
File "/home/toi/cours/mon_script.py", line 2
if 3 > 2
^
SyntaxError: expected ':'
```
Ici, **rien** ne s'exécute : Python a lu tout le fichier, vu l'erreur, et s'est arrêté. Les messages de syntaxe de Python 3.12+ sont très parlants : `expected ':'` veut dire « j'attendais deux-points ».
Ce chapitre parle surtout de la deuxième famille : les **exceptions**, qu'on peut anticiper, intercepter et même créer.
### 2. Lire un traceback (de bas en haut)
Prenons un vrai programme, enregistré dans `mon_script.py` :
```python
def calculer_moyenne(notes):
total = sum(notes)
return total / len(notes)
def afficher_bilan(eleve, notes):
moyenne = calculer_moyenne(notes)
print(f"{eleve} : {moyenne:.2f}")
afficher_bilan("Alice", [12, 15, 9])
afficher_bilan("Bob", [])
```
Exécution :
```
Alice : 12.00
Traceback (most recent call last):
File "/home/toi/cours/mon_script.py", line 12, in <module>
afficher_bilan("Bob", [])
~~~~~~~~~~~~~~^^^^^^^^^^^
File "/home/toi/cours/mon_script.py", line 7, in afficher_bilan
moyenne = calculer_moyenne(notes)
File "/home/toi/cours/mon_script.py", line 3, in calculer_moyenne
return total / len(notes)
~~~~~~^~~~~~~~~~~~
ZeroDivisionError: division by zero
```
Le mot *traceback* signifie « retracer le chemin ». Python te montre **la pile d'appels** : la suite de fonctions qui s'appelaient les unes les autres au moment du drame. L'en-tête le dit clairement : `most recent call last`, « l'appel le plus récent en dernier ». Donc **on lit de bas en haut** :
1. **La dernière ligne** est la plus importante : `ZeroDivisionError: division by zero`. Elle donne le **type** d'erreur (avant les deux-points) et un **message** (après). Ici : une division par zéro.
2. **Juste au-dessus**, le dernier bloc `File ...` indique **où** l'erreur s'est produite : fichier `mon_script.py`, **ligne 3**, dans la fonction `calculer_moyenne`, sur l'instruction `return total / len(notes)`. Les petits `~~~~^~~~` soulignent l'opération exacte qui a échoué : la division `/`.
3. **En remontant encore**, tu vois **qui a appelé** cette fonction : `afficher_bilan`, ligne 7.
4. **Tout en haut**, le point de départ : la ligne 12 du programme principal (`<module>` désigne le code qui n'est dans aucune fonction), avec l'appel `afficher_bilan("Bob", [])`.
On reconstitue l'histoire : *la ligne 12 appelle `afficher_bilan` avec une liste vide → qui appelle `calculer_moyenne` → qui divise par `len([])`, c'est-à-dire 0 → boum.*
> 💡 **Analogie** : un traceback, c'est comme une enquête policière racontée à l'envers. La dernière ligne, c'est le crime. Les blocs au-dessus, ce sont les témoins, du plus proche au plus lointain. Le coupable n'est pas toujours là où le crime a eu lieu : ici, la division est « innocente », c'est la ligne 12 qui a envoyé une liste vide !
**Méthode en 3 questions** devant n'importe quel traceback :
| Question | Où regarder |
|---|---|
| *Quoi ?* Quel type d'erreur ? | Dernière ligne, avant `:` |
| *Où ?* Quelle ligne de **mon** code ? | Le bloc `File` le plus bas qui pointe vers **ton** fichier |
| *Pourquoi ?* D'où viennent les mauvaises valeurs ? | Remonter les blocs `File` vers le haut |
> ⚠️ Quand tu utilises une bibliothèque, certains blocs `File` pointent vers des fichiers que tu n'as pas écrits (par exemple `.../lib/python3.12/json/decoder.py`). Ignore-les dans un premier temps : cherche **le dernier bloc qui mentionne ton fichier**. Le bug est presque toujours là.
### 3. Le tour des exceptions courantes
Voici les exceptions que tu croiseras 95 % du temps. Chaque exemple a été exécuté pour de vrai ; seule la dernière ligne du traceback est montrée.
#### `ValueError` — bon type, mauvaise valeur
```python
int("douze")
# ValueError: invalid literal for int() with base 10: 'douze'
float("3,5") # la virgule française ne passe pas !
# ValueError: could not convert string to float: '3,5'
```
`int()` attend bien une chaîne, mais pas *celle-là*. Très fréquent avec `input()`.
#### `TypeError` — mauvais type
```python
"Âge : " + 30
# TypeError: can only concatenate str (not "int") to str
len(5)
# TypeError: object of type 'int' has no len()
```
Tu essaies de faire une opération qui n'a pas de sens pour ce type. Solution typique : convertir (`str(30)`) ou utiliser une f-string.
#### `KeyError` — clé absente d'un dictionnaire
```python
prix = {"pomme": 1.2}
prix["poire"]
# KeyError: 'poire'
```
Le message affiche **la clé fautive**. Pense à `prix.get("poire")` ou au test `if "poire" in prix`.
#### `IndexError` — indice hors de la liste
```python
valeurs = [10, 20, 30]
valeurs[3]
# IndexError: list index out of range
```
Une liste de 3 éléments a les indices 0, 1 et 2. L'erreur classique « décalée de un ».
#### `FileNotFoundError` — fichier introuvable
```python
open("notes.txt")
# FileNotFoundError: [Errno 2] No such file or directory: 'notes.txt'
```
Soit le nom est faux, soit tu n'exécutes pas le script depuis le bon dossier (revois le [chapitre 07](07-fichiers-csv-json.md) sur les chemins).
#### `ZeroDivisionError` — division par zéro
```python
10 / 0
# ZeroDivisionError: division by zero
```
Vaut aussi pour `//` et `%`.
#### Deux autres très fréquentes
```python
print(totl) # faute de frappe dans le nom
# NameError: name 'totl' is not defined
"abc".push("d") # cette méthode n'existe pas pour les str
# AttributeError: 'str' object has no attribute 'push'
```
#### La hiérarchie des exceptions
Les exceptions sont rangées en **famille**, comme un arbre généalogique. Voici la partie qui t'intéresse :
```
BaseException
├── KeyboardInterrupt (Ctrl+C)
├── SystemExit (sys.exit())
└── Exception ← mère de toutes les erreurs « normales »
├── ArithmeticError
│ └── ZeroDivisionError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── OSError
│ └── FileNotFoundError
├── ValueError
├── TypeError
├── NameError
└── AttributeError
```
Pourquoi c'est utile ? Parce qu'**intercepter un parent intercepte aussi tous ses enfants**. `except LookupError` attrape à la fois les `KeyError` et les `IndexError`. Et `except Exception` attrape presque tout… ce qui est souvent **trop** (on y revient dans les pièges).
Tu peux vérifier la parenté avec `issubclass` :
```python
print(issubclass(KeyError, LookupError)) # True
print(issubclass(FileNotFoundError, OSError)) # True
print(issubclass(KeyboardInterrupt, Exception)) # False
```
### 4. Intercepter une exception : `try` / `except`
Jusqu'ici, une exception arrête net le programme. Avec `try` / `except`, tu dis à Python : « essaie ceci, et **si** telle erreur survient, fais plutôt cela ».
```python
texte = input("Ton âge : ")
try:
age = int(texte)
print(f"Dans 10 ans, tu auras {age + 10} ans.")
except ValueError:
print(f"« {texte} » n'est pas un nombre entier.")
print("Le programme continue.")
```
Avec l'entrée `vingt` :
```
Ton âge : vingt
« vingt » n'est pas un nombre entier.
Le programme continue.
```
Avec l'entrée `20` :
```
Ton âge : 20
Dans 10 ans, tu auras 30 ans.
Le programme continue.
```
Le déroulement :
- Python exécute le bloc `try` ligne par ligne.
- Si **aucune** erreur ne survient, le bloc `except` est **ignoré**.
- Si une `ValueError` survient, Python **abandonne le reste du bloc `try`** (le `print` n'est pas exécuté) et saute dans `except ValueError`.
- Si une **autre** erreur survient (par exemple une `TypeError`), elle n'est **pas** interceptée : le programme plante normalement, avec son traceback.
> 💡 **Analogie** : `try` / `except`, c'est le filet sous le trapéziste. Le trapéziste fait son numéro (`try`). S'il tombe d'une certaine façon prévue (`except ValueError`), le filet le rattrape. Mais un filet ne sert que pour les chutes prévues : si le chapiteau prend feu, le filet ne sert à rien.
#### Redemander jusqu'à obtenir une bonne valeur
Combiné avec une boucle `while`, c'est le schéma le plus utilisé du chapitre :
```python
while True:
try:
quantite = int(input("Quantité : "))
break # on ne sort que si la conversion a réussi
except ValueError:
print("Merci d'entrer un nombre entier.")
print(f"Tu as commandé {quantite} article(s).")
```
Avec les entrées `abc`, `2.5` puis `3` :
```
Quantité : abc
Merci d'entrer un nombre entier.
Quantité : 2.5
Merci d'entrer un nombre entier.
Quantité : 3
Tu as commandé 3 article(s).
```
#### Plusieurs `except`
Un même `try` peut avoir plusieurs blocs `except`, testés **dans l'ordre** :
```python
def lire_element(valeurs, saisie):
try:
indice = int(saisie)
return valeurs[indice]
except ValueError:
return "Il faut un nombre."
except IndexError:
return f"Indice entre 0 et {len(valeurs) - 1}, s'il te plaît."
fruits = ["pomme", "kiwi", "mangue"]
print(lire_element(fruits, "1")) # kiwi
print(lire_element(fruits, "deux")) # Il faut un nombre.
print(lire_element(fruits, "7")) # Indice entre 0 et 2, s'il te plaît.
```
Si deux erreurs doivent être traitées **de la même façon**, regroupe-les dans un **tuple** (les parenthèses sont obligatoires) :
```python
def diviser_textes(a, b):
try:
return int(a) / int(b)
except (ValueError, ZeroDivisionError):
return None
print(diviser_textes("10", "4")) # 2.5
print(diviser_textes("10", "0")) # None
print(diviser_textes("dix", "2")) # None
```
> ⚠️ L'ordre compte : si tu mets `except LookupError` **avant** `except KeyError`, le second ne sera jamais atteint, car le parent attrape déjà l'enfant. Mets toujours **les exceptions les plus précises en premier**.
### 5. Récupérer l'objet erreur avec `as`
Tu peux donner un nom à l'exception attrapée pour afficher son message :
```python
try:
int("abc")
except ValueError as erreur:
print("Type :", type(erreur).__name__)
print("Message :", erreur)
```
```
Type : ValueError
Message : invalid literal for int() with base 10: 'abc'
```
C'est pratique pour informer l'utilisateur **sans** lui montrer un traceback complet :
```python
from pathlib import Path
chemin = Path("fichier_qui_nexiste_pas.txt")
try:
contenu = chemin.read_text(encoding="utf-8")
except FileNotFoundError as erreur:
print(f"Impossible d'ouvrir le fichier : {erreur.filename}")
```
```
Impossible d'ouvrir le fichier : fichier_qui_nexiste_pas.txt
```
### 6. Les blocs `else` et `finally`
La structure complète ressemble à ceci :
```python
try:
... # le code « risqué »
except UneErreur:
... # exécuté SI l'erreur survient
else:
... # exécuté SI AUCUNE erreur n'est survenue
finally:
... # exécuté TOUJOURS, quoi qu'il arrive
```
#### `else` : « tout s'est bien passé »
Pourquoi ne pas tout mettre dans le `try` ? Parce qu'on veut que le `try` ne contienne **que** la ligne susceptible d'échouer. Sinon, on risque d'attraper par erreur une exception venant d'une autre ligne.
```python
import json
from pathlib import Path
chemin = Path("config.json")
chemin.write_text('{"theme": "sombre", "taille": 14}', encoding="utf-8")
try:
texte = chemin.read_text(encoding="utf-8")
except FileNotFoundError:
print("Pas de configuration, on utilise les valeurs par défaut.")
config = {}
else:
config = json.loads(texte)
print("Configuration chargée.")
print(config)
```
```
Configuration chargée.
{'theme': 'sombre', 'taille': 14}
```
#### `finally` : « quoi qu'il arrive »
Le bloc `finally` s'exécute **toujours** : succès, erreur interceptée, erreur non interceptée, et même `return` dans le `try`. On s'en sert pour faire le ménage (fermer une connexion, afficher un message de fin…).
```python
def diviser(a, b):
try:
resultat = a / b
except ZeroDivisionError:
print("Division par zéro évitée.")
resultat = None
else:
print("Division réussie.")
finally:
print("--- fin du calcul ---")
return resultat
print(diviser(10, 2))
print(diviser(1, 0))
```
```
Division réussie.
--- fin du calcul ---
5.0
Division par zéro évitée.
--- fin du calcul ---
None
```
> 💡 Pour les fichiers, `with open(...)` (vu au [chapitre 07](07-fichiers-csv-json.md)) fait déjà le ménage à ta place : il ferme le fichier même en cas d'erreur. C'est un `finally` intégré !
### 7. Lever une exception : `raise`
Jusqu'ici, on **subissait** les exceptions. Tu peux aussi les **déclencher** toi-même avec `raise`, pour signaler qu'une situation est anormale.
```python
def calculer_moyenne(notes):
if not notes:
raise ValueError("Impossible de calculer la moyenne d'une liste vide.")
return sum(notes) / len(notes)
print(calculer_moyenne([12, 15, 9]))
print(calculer_moyenne([]))
```
```
12.0
Traceback (most recent call last):
File "/home/toi/cours/mon_script.py", line 8, in <module>
print(calculer_moyenne([]))
~~~~~~~~~~~~~~~~^^^^
File "/home/toi/cours/mon_script.py", line 3, in calculer_moyenne
raise ValueError("Impossible de calculer la moyenne d'une liste vide.")
ValueError: Impossible de calculer la moyenne d'une liste vide.
```
Pourquoi lever une erreur plutôt que de renvoyer `None` ou d'afficher un message ? Parce qu'une fonction **ne sait pas** comment l'appelant veut réagir. Peut-être qu'il veut redemander une saisie, peut-être écrire dans un journal, peut-être arrêter. En levant une exception, la fonction dit « je ne peux pas faire mon travail, voici pourquoi » et **laisse l'appelant décider** :
```python
def calculer_moyenne(notes):
if not notes:
raise ValueError("Impossible de calculer la moyenne d'une liste vide.")
return sum(notes) / len(notes)
classe = {"Alice": [12, 15, 9], "Bob": [], "Chloé": [18, 16]}
for eleve, notes in classe.items():
try:
print(f"{eleve} : {calculer_moyenne(notes):.2f}")
except ValueError as erreur:
print(f"{eleve} : pas de moyenne ({erreur})")
```
```
Alice : 12.00
Bob : pas de moyenne (Impossible de calculer la moyenne d'une liste vide.)
Chloé : 17.00
```
#### Relancer une exception
Dans un `except`, un `raise` **seul** relance l'exception en cours. Utile pour noter qu'il y a eu un problème sans l'étouffer :
```python
def convertir(texte):
try:
return int(texte)
except ValueError:
print(f"[journal] conversion ratée pour {texte!r}")
raise # on laisse remonter l'erreur d'origine
try:
convertir("12a")
except ValueError:
print("L'appelant a bien reçu l'erreur.")
```
```
[journal] conversion ratée pour '12a'
L'appelant a bien reçu l'erreur.
```
Tu peux aussi **transformer** une erreur en une autre plus parlante, en gardant la trace de l'originale avec `from` :
```python
try:
int("x")
except ValueError as erreur:
raise RuntimeError("Lecture du fichier de scores impossible") from erreur
```
Le traceback affichera alors les deux erreurs, reliées par la phrase *The above exception was the direct cause of the following exception*.
### 8. Créer tes propres exceptions
Les exceptions intégrées sont génériques. Dans un vrai programme, il est souvent plus clair d'avoir des erreurs **qui parlent ton langage métier** : `SoldeInsuffisantError`, `MotDePasseTropCourtError`…
Pour cela, on utilise une **classe**. Les classes seront expliquées en détail au [chapitre 10](10-poo-classes-objets.md) ; pour l'instant, retiens simplement cette **syntaxe à apprendre par cœur** :
```python
class SoldeInsuffisantError(Exception):
pass
```
Lis-la comme : « je crée un nouveau type d'erreur, appelé `SoldeInsuffisantError`, qui est une sorte d'`Exception` ». Le `pass` signifie « rien d'autre à ajouter ». Par convention, le nom se termine par `Error`.
Tu l'utilises exactement comme une exception intégrée :
```python
class SoldeInsuffisantError(Exception):
pass
def retirer(solde, montant):
if montant > solde:
raise SoldeInsuffisantError(
f"Solde insuffisant : {solde} € disponibles, {montant} € demandés"
)
return solde - montant
solde = 50
try:
solde = retirer(solde, 80)
except SoldeInsuffisantError as erreur:
print("Opération refusée.", erreur)
print(f"Solde final : {solde} €")
```
```
Opération refusée. Solde insuffisant : 50 € disponibles, 80 € demandés
Solde final : 50 €
```
Tu peux même créer **ta propre petite famille** : une erreur « mère » pour ton programme, et des erreurs « filles » plus précises. L'appelant pourra alors attraper toutes les erreurs de ton programme d'un coup avec la mère :
```python
class BanqueError(Exception):
pass
class SoldeInsuffisantError(BanqueError):
pass
class MontantInvalideError(BanqueError):
pass
def retirer(solde, montant):
if montant <= 0:
raise MontantInvalideError("Le montant doit être positif.")
if montant > solde:
raise SoldeInsuffisantError("Solde insuffisant.")
return solde - montant
for montant in [20, -5, 500]:
try:
print("Nouveau solde :", retirer(100, montant))
except BanqueError as erreur:
print(f"{type(erreur).__name__} : {erreur}")
```
```
Nouveau solde : 80
MontantInvalideError : Le montant doit être positif.
SoldeInsuffisantError : Solde insuffisant.
```
### 9. Deux philosophies : vérifier avant, ou demander pardon après
Il y a deux façons de gérer un cas problématique :
- **LBYL** (*Look Before You Leap*, « regarde avant de sauter ») : on teste avec un `if` avant d'agir.
- **EAFP** (*Easier to Ask Forgiveness than Permission*, « plus facile de demander pardon que la permission ») : on agit, et on intercepte l'erreur si elle survient.
```python
stock = {"pomme": 12, "kiwi": 0}
# Style LBYL
if "banane" in stock:
print(stock["banane"])
else:
print("Produit inconnu")
# Style EAFP
try:
print(stock["banane"])
except KeyError:
print("Produit inconnu")
```
Les deux sont valables. En Python, le style EAFP est très courant, surtout quand la vérification préalable est compliquée ou incomplète. Exemple : vérifier qu'une chaîne représente un entier valide (`"-12"`, `" 7 "`, `"+3"`…) est pénible avec des `if`, alors que `try: int(texte)` règle la question en une ligne.
Règle pratique : **utilise `if` pour les cas prévisibles et fréquents** (un dictionnaire où la clé manque souvent → `.get()`), et **`try` pour les cas exceptionnels ou difficiles à prévoir** (saisie utilisateur, fichiers, réseau).
### 10. Déboguer : trouver les bugs qui ne plantent pas
Le pire bug n'est pas celui qui plante : c'est celui qui **donne un résultat faux sans rien dire**. Pas de traceback pour t'aider ! Il faut alors enquêter. Voici trois outils, du plus simple au plus puissant.
Prenons un programme bogué :
```python
def appliquer_remise(prix, pourcentage):
remise = prix * pourcentage // 100
return prix - remise
panier = [19.99, 5.50, 12.00]
total = 0
for prix in panier:
total = appliquer_remise(prix, 10)
print(f"Total après remise : {total:.2f} €")
```
```
Total après remise : 11.00 €
```
Le total devrait être environ 34,24 €. Quelque chose cloche… et même deux choses.
#### Outil n° 1 : le *print-debugging*
La méthode la plus ancienne et toujours efficace : ajouter des `print` pour **voir** les valeurs à chaque étape.
```python
def appliquer_remise(prix, pourcentage):
remise = prix * pourcentage // 100
print(f"[DEBUG] prix={prix} remise={remise}")
return prix - remise
panier = [19.99, 5.50, 12.00]
total = 0
for prix in panier:
total = appliquer_remise(prix, 10)
print(f"[DEBUG] total={total}")
print(f"Total après remise : {total:.2f} €")
```
```
[DEBUG] prix=19.99 remise=1.0
[DEBUG] total=18.99
[DEBUG] prix=5.5 remise=0.0
[DEBUG] total=5.5
[DEBUG] prix=12.0 remise=1.0
[DEBUG] total=11.0
Total après remise : 11.00 €
```
On voit immédiatement deux bugs :
1. `remise=1.0` pour 19,99 € au lieu de 1,999 : c'est `//` (division entière) au lieu de `/`.
2. `total` ne s'accumule pas : il est **remplacé** à chaque tour (`total =` au lieu de `total +=`).
Astuce : la f-string avec `=` affiche le nom **et** la valeur, idéal pour déboguer :
```python
prix = 19.99
pourcentage = 10
print(f"{prix=} {pourcentage=} {prix * pourcentage / 100=}")
```
```
prix=19.99 pourcentage=10 prix * pourcentage / 100=1.9989999999999997
```
(Le `1.9989999999999997` au lieu de `1.999` est normal : c'est l'imprécision des nombres décimaux en machine, vue au [chapitre 01](01-variables-types-operateurs.md).)
> ⚠️ Pense à **retirer tes `print` de débogage** une fois le bug corrigé. Les préfixer par `[DEBUG]` permet de les retrouver facilement avec la recherche de VS Code (Ctrl+F).
#### Outil n° 2 : `breakpoint()` et le débogueur `pdb`
Ajouter des `print` partout devient vite lourd. Python possède un **débogueur intégré** : `pdb`. Il suffit d'écrire `breakpoint()` à l'endroit où tu veux **mettre le programme en pause** :
```python
def appliquer_remise(prix, pourcentage):
breakpoint()
remise = prix * pourcentage / 100
return prix - remise
total = appliquer_remise(80, 25)
print(total)
```
Quand tu lances le script, il s'arrête et te donne une invite `(Pdb)` où tu peux taper des commandes :
```
> /home/toi/cours/mon_script.py(2)appliquer_remise()
-> breakpoint()
(Pdb) p prix
80
(Pdb) p pourcentage
25
(Pdb) n
> /home/toi/cours/mon_script.py(3)appliquer_remise()
-> remise = prix * pourcentage / 100
(Pdb) p remise
*** NameError: name 'remise' is not defined
(Pdb) n
> /home/toi/cours/mon_script.py(4)appliquer_remise()
-> return prix - remise
(Pdb) p remise
20.0
(Pdb) c
60.0
```
La ligne marquée `->` est **la prochaine ligne qui va s'exécuter** (pas encore exécutée !). C'est pour ça que `remise` n'existe pas encore au premier essai.
Les commandes essentielles :
| Commande | Signification |
|---|---|
| `p variable` | **p**rint : affiche la valeur d'une expression |
| `n` | **n**ext : exécute la ligne courante et passe à la suivante (sans entrer dans les fonctions appelées) |
| `s` | **s**tep : entre **dans** la fonction appelée sur la ligne courante |
| `l` | **l**ist : affiche le code autour de la ligne courante |
| `c` | **c**ontinue : reprend l'exécution normale jusqu'au prochain `breakpoint()` |
| `q` | **q**uit : arrête le programme |
> 💡 N'oublie pas d'enlever `breakpoint()` avant de partager ton code, sinon il se mettra en pause chez tout le monde !
#### Outil n° 3 : le débogueur de VS Code
C'est le même principe que `pdb`, mais avec une interface graphique. C'est l'outil que tu utiliseras le plus au quotidien.
**1. Poser un point d'arrêt.** Dans VS Code, clique dans la marge **à gauche des numéros de ligne** : un **point rouge** apparaît. Le programme s'arrêtera **juste avant** d'exécuter cette ligne. (Raccourci : place le curseur sur la ligne et appuie sur **F9**.)
**2. Lancer en mode débogage.** Au lieu du bouton ▶ « Run », clique sur la petite flèche à côté et choisis **« Python Debugger: Debug Python File »**, ou appuie sur **F5** (choisis « Python File » si VS Code te demande une configuration). L'extension *Python* de Microsoft (installée au [chapitre 00](00-installation-environnement.md)) inclut le débogueur.
**3. Observer.** Quand le programme s'arrête sur ton point rouge, la ligne est surlignée en jaune et le panneau de gauche affiche :
- **VARIABLES** : toutes les variables locales et globales avec leur valeur actuelle. Tu peux déplier les listes et dictionnaires.
- **WATCH** (Espion) : ajoute des expressions à surveiller en continu, par exemple `total` ou `prix * 0.9`.
- **CALL STACK** (Pile des appels) : la même information qu'un traceback, en direct.
Tu peux aussi survoler une variable avec la souris dans le code pour voir sa valeur.
**4. Avancer pas à pas.** Une barre d'outils flottante apparaît en haut :
| Bouton | Raccourci | Effet | Équivalent `pdb` |
|---|---|---|---|
| ▶ Continuer | F5 | Jusqu'au prochain point d'arrêt | `c` |
| ⤼ Pas à pas principal (*Step Over*) | F10 | Ligne suivante, sans entrer dans les fonctions | `n` |
| ⤓ Pas à pas détaillé (*Step Into*) | F11 | Entre dans la fonction appelée | `s` |
| ⤒ Pas à pas sortant (*Step Out*) | Maj+F11 | Termine la fonction courante et remonte | `r` |
| ⟳ Redémarrer | Ctrl+Maj+F5 | Relance depuis le début | — |
| ■ Arrêter | Maj+F5 | Arrête le programme | `q` |
**5. Bonus : la console de débogage.** Pendant la pause, l'onglet **DEBUG CONSOLE** permet de taper n'importe quelle expression Python (`len(panier)`, `prix * 2`…) et de voir le résultat, dans le contexte exact du programme en pause.
**6. Points d'arrêt conditionnels.** Clic droit sur un point rouge → **Edit Breakpoint** → **Expression**, par exemple `prix > 10`. Le programme ne s'arrêtera que si la condition est vraie. Indispensable dans une boucle de 10 000 tours !
> 💡 **Analogie** : le débogueur, c'est la télécommande d'un film. Tu peux mettre sur pause (point d'arrêt), avancer image par image (F10), zoomer sur un détail (F11, Variables), ou reprendre la lecture (F5). Le `print`, c'est comme prendre des photos du film en espérant avoir capturé le bon moment.
### 11. `assert` : vérifier tes hypothèses
`assert` vérifie qu'une condition est vraie. Si elle est fausse, il lève une `AssertionError` avec le message fourni :
```python
def calculer_remise(prix, pourcentage):
assert 0 <= pourcentage <= 100, f"pourcentage invalide : {pourcentage}"
return prix * pourcentage / 100
print(calculer_remise(50, 20))
print(calculer_remise(50, 150))
```
```
10.0
Traceback (most recent call last):
File "/home/toi/cours/mon_script.py", line 7, in <module>
print(calculer_remise(50, 150))
~~~~~~~~~~~~~~~^^^^^^^^^
File "/home/toi/cours/mon_script.py", line 2, in calculer_remise
assert 0 <= pourcentage <= 100, f"pourcentage invalide : {pourcentage}"
^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: pourcentage invalide : 150
```
`assert` sert à **vérifier tes propres hypothèses de programmeur** (« à cet endroit, cette liste ne peut pas être vide »), et à écrire de petits tests rapides :
```python
def inverser(texte):
return texte[::-1]
assert inverser("abc") == "cba"
assert inverser("") == ""
assert inverser("été") == "été"
print("Tous les tests passent.")
```
```
Tous les tests passent.
```
C'est d'ailleurs exactement ce qu'utilise l'outil de test `pytest` que tu découvriras au [chapitre 13](13-tests-pytest-bonnes-pratiques.md).
> ⚠️ **Ne valide jamais une saisie utilisateur avec `assert`**. Python peut être lancé avec l'option `-O` (optimisation) qui **désactive tous les `assert`**. Pour une donnée venant de l'extérieur (saisie, fichier), utilise `if` + `raise ValueError(...)`.
| Situation | Outil |
|---|---|
| La donnée vient de l'utilisateur, d'un fichier, du réseau | `if ...: raise ValueError(...)` |
| Tu vérifies que **ton propre code** est cohérent | `assert` |
| Tu écris un test | `assert` (puis `pytest`) |
### 12. Une méthode de débogage en 5 étapes
Quand un bug te résiste, ne modifie pas le code au hasard. Procède comme un détective :
1. **Reproduire** : trouve les entrées exactes qui provoquent le bug. Un bug qu'on ne sait pas reproduire ne se corrige pas.
2. **Lire** : s'il y a un traceback, applique la méthode des 3 questions (quoi, où, pourquoi).
3. **Isoler** : réduis le problème. Commente des morceaux, teste la fonction suspecte seule avec des valeurs simples.
4. **Observer** : `print`, `breakpoint()` ou VS Code pour comparer **ce que tu crois** que contiennent tes variables avec **ce qu'elles contiennent vraiment**.
5. **Corriger puis vérifier** : corrige **une seule chose à la fois**, relance, et ajoute un `assert` pour que le bug ne revienne pas.
> 💡 **La méthode du canard en plastique** : explique ton code ligne par ligne, à voix haute, à un canard en plastique (ou à un ami patient). Très souvent, tu trouves le bug toi-même en cours d'explication, parce que tu es obligé de dire ce que fait *vraiment* chaque ligne.
---
## Pièges fréquents
### Piège 1 — Le `except` nu qui avale tout
```python
# ❌ À NE PAS FAIRE
def lire_age(texte):
try:
return int(texte)
except:
return 0
print(lire_age("25"))
print(lire_age(None)) # bug caché : on reçoit 0 au lieu d'une erreur claire
```
Un `except:` sans type attrape **absolument tout** : les fautes de frappe (`NameError`), les mauvais types (`TypeError`), et même **Ctrl+C** (`KeyboardInterrupt`) ! Ton programme ne plante plus… mais il produit des résultats faux en silence, et tu ne peux même plus l'interrompre proprement. C'est le piège n° 1 des débutants.
```python
# ✅ Attrape uniquement ce que tu sais gérer
def lire_age(texte):
try:
return int(texte)
except ValueError:
return 0
```
Même chose pour `except Exception:` : à réserver au tout dernier filet de sécurité d'un programme, en **affichant** l'erreur, jamais pour l'étouffer.
### Piège 2 — Oublier les parenthèses pour plusieurs exceptions
```python
try:
int("x")
except ValueError, TypeError:
pass
```
```
except ValueError, TypeError:
^^^^^^^^^^^^^^^^^^^^^
SyntaxError: multiple exception types must be parenthesized
```
✅ Correction : `except (ValueError, TypeError):`.
### Piège 3 — Un `try` trop large
```python
# ❌ Tout le bloc est dans le try
notes = {"Alice": 14, "Bob": 12}
try:
nom = "Chloé"
note = notes[nom]
moyenne = sum(notes.values()) / len(note) # bug : len(note) au lieu de len(notes)
print(moyenne)
except Exception:
print("Élève introuvable")
```
```
Élève introuvable
```
Le message est **faux** et trompeur : ici c'est bien le `KeyError` qui se déclenche, mais si Chloé existait, le bug `len(note)` (une `TypeError`) serait lui aussi transformé en « Élève introuvable ». Tu chercherais l'erreur au mauvais endroit pendant des heures.
```python
# ✅ Le try ne contient que la ligne risquée, et l'exception précise
notes = {"Alice": 14, "Bob": 12}
nom = "Chloé"
try:
note = notes[nom]
except KeyError:
print(f"Élève introuvable : {nom}")
else:
print(f"{nom} a {note}/20")
```
### Piège 4 — Mettre le parent avant l'enfant
```python
donnees = {"a": 1}
try:
donnees["b"]
except LookupError:
print("Problème de recherche")
except KeyError:
print("Clé absente") # ❌ jamais atteint
```
```
Problème de recherche
```
`KeyError` est une sorte de `LookupError`, donc le premier `except` gagne toujours. ✅ Mets les exceptions **les plus précises en premier**.
### Piège 5 — Lever une simple chaîne avec `raise`
```python
# ❌ raise avec une simple chaîne
raise "Erreur grave"
```
```
TypeError: exceptions must derive from BaseException
```
On ne peut lever que des **objets exception**. ✅ `raise ValueError("Erreur grave")`.
### Piège 6 — Valider des données avec `assert`
```python
# ❌ disparaît si Python est lancé avec -O
def definir_age(age):
assert age >= 0, "âge négatif"
return age
```
✅ Pour une donnée extérieure :
```python
def definir_age(age):
if age < 0:
raise ValueError(f"âge négatif : {age}")
return age
```
### Piège 7 — Oublier de convertir la saisie (et croire à un bug mystérieux)
```python
age = input("Âge : ")
print(age + 1)
```
```
TypeError: can only concatenate str (not "int") to str
```
`input()` renvoie **toujours** une chaîne. Le message dit « on ne peut concaténer que des str à une str » : `age` est donc une chaîne. ✅ `age = int(input("Âge : "))`, protégé par un `try` / `except ValueError`.
### Piège 8 — Afficher l'erreur puis continuer comme si de rien n'était
```python
try:
prix = float("douze")
except ValueError:
print("Prix invalide")
print(prix * 2)
```
```
Prix invalide
Traceback (most recent call last):
...
NameError: name 'prix' is not defined
```
Après l'`except`, la variable `prix` n'a jamais été créée. ✅ Soit tu lui donnes une valeur par défaut dans l'`except`, soit tu arrêtes le traitement (`return`, `continue`, boucle de nouvelle saisie…).
---
## Exercices
### Exercice 1 ⭐ — Saisie d'un entier
Écris une fonction `demander_entier(message)` qui affiche `message`, lit la saisie de l'utilisateur et la renvoie convertie en `int`. Si la saisie n'est pas un entier, elle affiche `Ce n'est pas un nombre entier, recommence.` et redemande, autant de fois que nécessaire.
```
Ton âge : vingt
Ce n'est pas un nombre entier, recommence.
Ton âge : 20
Tu as 20 ans.
```
### Exercice 2 ⭐ — Division sans plantage
Écris une fonction `division_sure(a, b)` qui renvoie `a / b`, ou la chaîne `"Division par zéro impossible"` si `b` vaut 0. Utilise `try` / `except`, pas de `if`.
```python
print(division_sure(10, 4)) # 2.5
print(division_sure(3, 0)) # Division par zéro impossible
```
### Exercice 3 ⭐ — Lire un traceback
Sans exécuter ce programme, réponds aux trois questions : quel est le type d'erreur ? Quelle ligne de code l'a déclenchée ? Quelle est la cause et comment la corriger ?
```python
def prix_ttc(prix_ht):
return prix_ht * 1.2
def afficher_facture(articles):
for nom, prix in articles.items():
print(f"{nom} : {prix_ttc(prix):.2f} €")
panier = {"stylo": 1.5, "cahier": "3.2", "gomme": 0.8}
afficher_facture(panier)
```
```
stylo : 1.80 €
Traceback (most recent call last):
File "/home/toi/cours/facture.py", line 11, in <module>
afficher_facture(panier)
~~~~~~~~~~~~~~~~^^^^^^^^
File "/home/toi/cours/facture.py", line 7, in afficher_facture
print(f"{nom} : {prix_ttc(prix):.2f} €")
~~~~~~~~^^^^^^
File "/home/toi/cours/facture.py", line 2, in prix_ttc
return prix_ht * 1.2
~~~~~~~~^~~~~
TypeError: can't multiply sequence by non-int of type 'float'
```
### Exercice 4 ⭐⭐ — Nettoyer une liste de notes
Écris une fonction `convertir_notes(textes)` qui reçoit une liste de chaînes et renvoie un tuple `(notes_valides, rejets)` :
- `notes_valides` : la liste des notes converties en `float` ;
- `rejets` : la liste des chaînes qui n'ont pas pu être converties **ou** dont la valeur n'est pas entre 0 et 20.
Bonus : accepte la virgule française (`"12,5"`).
```python
valides, rejets = convertir_notes(["12", "15.5", "abc", "12,5", "25", "", "-1", "8"])
print(valides) # [12.0, 15.5, 12.5, 8.0]
print(rejets) # ['abc', '25', '', '-1']
```
### Exercice 5 ⭐⭐ — Chargement de configuration
Écris une fonction `charger_config(chemin)` qui lit un fichier JSON et renvoie le dictionnaire obtenu. Elle doit :
- renvoyer `{"langue": "fr", "volume": 50}` (configuration par défaut) si le fichier n'existe pas, en affichant `Fichier absent, configuration par défaut.` ;
- renvoyer la même configuration par défaut si le fichier n'est pas du JSON valide (l'exception s'appelle `json.JSONDecodeError`), en affichant `Fichier corrompu, configuration par défaut.` ;
- dans les deux cas, ne jamais planter.
### Exercice 6 ⭐⭐ — Inventaire et exceptions précises
On a un stock : `stock = {"pomme": 10, "poire": 4, "kiwi": 0}`. Écris une fonction `retirer_du_stock(stock, produit, quantite)` qui :
- lève une `KeyError` (laisse-la se produire naturellement) si le produit n'existe pas ;
- lève une `ValueError("quantité invalide : ...")` si la quantité est négative ou nulle ;
- lève une `ValueError("stock insuffisant pour ...")` si la quantité demandée dépasse le stock ;
- sinon, met à jour le stock.
Puis écris une boucle qui teste ces demandes et affiche un message pour chacune :
```python
demandes = [("pomme", 3), ("banane", 1), ("poire", 10), ("kiwi", -2), ("poire", 4)]
```
```
OK : 3 pomme(s) retirée(s)
Erreur : produit inconnu 'banane'
Erreur : stock insuffisant pour poire (4 disponible(s))
Erreur : quantité invalide : -2
OK : 4 poire(s) retirée(s)
Stock final : {'pomme': 7, 'poire': 0, 'kiwi': 0}
```
### Exercice 7 ⭐⭐⭐ — Ta propre exception
Crée une exception `MotDePasseInvalideError`. Écris une fonction `verifier_mot_de_passe(mdp)` qui lève cette exception avec un message précis si :
- le mot de passe fait moins de 8 caractères ;
- il ne contient aucun chiffre ;
- il ne contient aucune majuscule.
Sinon, elle renvoie `True`. Écris ensuite un programme qui demande un mot de passe jusqu'à ce qu'il soit valide.
```
Mot de passe : abc
Refusé : au moins 8 caractères (actuellement 3)
Mot de passe : abcdefgh
Refusé : il faut au moins un chiffre
Mot de passe : abcdefg1
Refusé : il faut au moins une majuscule
Mot de passe : Abcdefg1
Mot de passe accepté !
```
### Exercice 8 ⭐⭐⭐ — Chasse aux bugs
Ce programme est censé compter les mots d'un texte et afficher les 3 plus fréquents. Il contient **trois bugs** (dont un seul fait planter le programme). Trouve-les avec `print`, `breakpoint()` ou le débogueur de VS Code, et corrige-les. Pour chacun, note la technique qui t'a permis de le trouver.
```python
def compter_mots(texte):
compteur = {}
for mot in texte.split():
mot = mot.lower().strip(".,!?")
if mot in compteur:
compteur[mot] = 1
else:
compteur[mot] = 1
return compteur
def top_mots(compteur, n):
classement = sorted(compteur.items(), key=lambda paire: paire[1])
return classement[:n]
texte = "Le chat dort. Le chien dort aussi. Le chat rêve !"
for mot, nombre in top_mots(compter_mots(texte), 3):
print(f"{mot} : {nombre}")
print("Mot le plus fréquent :", top_mots(compter_mots(texte), 3)[3])
```
Résultat attendu après correction :
```
le : 3
chat : 2
dort : 2
Mot le plus fréquent : le
```
---
## Mini-projet — Une calculatrice qui ne plante jamais
### Cahier des charges
Tu vas écrire `calculatrice.py`, une calculatrice en ligne de commande **impossible à faire planter**, quoi que tape l'utilisateur.
1. Le programme affiche une invite `> ` et lit une expression de la forme `nombre opérateur nombre`, séparée par des espaces : `12 + 4`, `7.5 * 2`, `2 ** 10`.
2. Opérateurs acceptés : `+`, `-`, `*`, `/`, `//`, `%`, `**`.
3. Les nombres peuvent être décimaux, avec un point **ou** une virgule (`3,5`).
4. Le résultat est affiché ; s'il est entier (par exemple `4.0`), on l'affiche sans décimale (`4`).
5. Le mot-clé `ans` peut remplacer un nombre : il vaut le dernier résultat calculé (0 au départ).
6. Commandes spéciales : `historique` (affiche tous les calculs réussis), `aide`, `quitter`.
7. Toute erreur affiche un message clair commençant par `Erreur :` et le programme continue :
- format incorrect (pas 3 éléments) ;
- nombre invalide ;
- opérateur inconnu ;
- division par zéro ;
- résultat trop grand (`10 ** 10000` en `float` → `OverflowError`).
8. **Ctrl+C** et **Ctrl+D** (fin de saisie) quittent proprement avec `Au revoir !`, sans traceback.
9. Le code définit une exception personnalisée `ExpressionInvalideError` pour les erreurs de format.
Exemple de session :
```
Calculatrice robuste — tape 'aide' pour l'aide, 'quitter' pour sortir.
> 12 + 4
= 16
> ans * 2,5
= 40
> 10 / 0
Erreur : division par zéro.
> 3 $ 4
Erreur : opérateur inconnu '$'.
> douze + 1
Erreur : 'douze' n'est pas un nombre.
> 5 +
Erreur : il faut 3 éléments séparés par des espaces (ex : 3 + 4).
> 2.0 ** 10000
Erreur : résultat trop grand.
> historique
1. 12 + 4 = 16
2. 16 * 2.5 = 40
> quitter
Au revoir !
```
### Étapes suggérées
1. Écris `convertir_nombre(texte, dernier_resultat)` : gère `ans`, remplace `,` par `.`, appelle `float()` et transforme la `ValueError` en `ExpressionInvalideError` avec un message clair.
2. Écris `calculer(a, operateur, b)` : un dictionnaire ou un `match` qui associe chaque opérateur à son calcul. Lève `ExpressionInvalideError` si l'opérateur est inconnu. Laisse `ZeroDivisionError` et `OverflowError` se produire naturellement.
3. Écris `analyser(ligne, dernier_resultat)` qui découpe la ligne, vérifie qu'il y a 3 morceaux et renvoie `(a, operateur, b)`.
4. Écris `formater(nombre)` qui affiche `16` plutôt que `16.0`.
5. Écris la boucle principale avec **un seul** `try` qui attrape chaque exception séparément, avec le bon message.
6. Entoure le tout d'un `try` / `except (KeyboardInterrupt, EOFError)`.
7. Teste **toutes** les erreurs de la liste : c'est le cœur du projet !
### Pistes d'amélioration
- Accepter les expressions sans espaces (`12+4`) en analysant la chaîne caractère par caractère.
- Ajouter des fonctions à un argument : `racine 16`, `abs -3`.
- Sauvegarder l'historique dans un fichier JSON à la sortie et le recharger au démarrage (en gérant le fichier absent ou corrompu !).
- Compter les erreurs par type et les afficher en quittant.
---
## Corrigés
<details><summary>Corrigé de l'exercice 1</summary>
```python
def demander_entier(message):
while True:
saisie = input(message)
try:
return int(saisie) # return sort à la fois de la boucle et de la fonction
except ValueError:
print("Ce n'est pas un nombre entier, recommence.")
age = demander_entier("Ton âge : ")
print(f"Tu as {age} ans.")
```
Le `return` à l'intérieur du `try` n'est atteint que si `int(saisie)` a réussi. En cas d'échec, on passe dans l'`except`, puis la boucle `while True` recommence. Remarque qu'on n'a pas besoin de `break` : `return` suffit.
</details>
<details><summary>Corrigé de l'exercice 2</summary>
```python
def division_sure(a, b):
try:
return a / b
except ZeroDivisionError:
return "Division par zéro impossible"
print(division_sure(10, 4))
print(division_sure(3, 0))
```
```
2.5
Division par zéro impossible
```
Dans un vrai programme, renvoyer tantôt un nombre, tantôt une chaîne n'est pas idéal (l'appelant doit vérifier le type). Une meilleure conception consiste souvent à laisser l'exception remonter, ou à renvoyer `None`.
</details>
<details><summary>Corrigé de l'exercice 3</summary>
1. **Quoi ?** Une `TypeError` : *can't multiply sequence by non-int of type 'float'*, « impossible de multiplier une séquence par un non-entier de type float ». Une « séquence » ici, c'est une chaîne.
2. **Où ?** Le bloc le plus bas : ligne 2, dans `prix_ttc`, sur `prix_ht * 1.2`.
3. **Pourquoi ?** En remontant : `prix_ttc` a été appelée ligne 7 avec `prix`, qui vient du dictionnaire `panier`. Le premier article (`stylo`) est passé, c'est donc un article suivant qui pose problème : `"cahier": "3.2"` est une **chaîne**, pas un nombre ! (Une chaîne multipliée par un entier fonctionne, `"ab" * 2` donne `"abab"`, mais pas par un `float`.)
Correction : mettre `"cahier": 3.2` dans les données. Si les données viennent d'un fichier qu'on ne contrôle pas, on peut convertir dans `prix_ttc` :
```python
def prix_ttc(prix_ht):
return float(prix_ht) * 1.2
def afficher_facture(articles):
for nom, prix in articles.items():
print(f"{nom} : {prix_ttc(prix):.2f} €")
panier = {"stylo": 1.5, "cahier": "3.2", "gomme": 0.8}
afficher_facture(panier)
```
```
stylo : 1.80 €
cahier : 3.84 €
gomme : 0.96 €
```
Leçon : le crime a eu lieu ligne 2, mais le coupable était dans les **données** de la ligne 10.
</details>
<details><summary>Corrigé de l'exercice 4</summary>
```python
def convertir_notes(textes):
notes_valides = []
rejets = []
for texte in textes:
try:
note = float(texte.replace(",", "."))
except ValueError:
rejets.append(texte)
continue # on passe directement à la chaîne suivante
if 0 <= note <= 20:
notes_valides.append(note)
else:
rejets.append(texte)
return notes_valides, rejets
valides, rejets = convertir_notes(["12", "15.5", "abc", "12,5", "25", "", "-1", "8"])
print(valides)
print(rejets)
```
```
[12.0, 15.5, 12.5, 8.0]
['abc', '25', '', '-1']
```
Deux types de rejets : ceux qui ne sont pas des nombres (exception `ValueError`, y compris la chaîne vide), et ceux qui sont des nombres hors limites (simple `if`). Le `continue` évite d'utiliser une variable `note` qui n'existerait pas (voir le piège 8). On aurait aussi pu utiliser un bloc `else` sur le `try`.
</details>
<details><summary>Corrigé de l'exercice 5</summary>
```python
import json
from pathlib import Path
CONFIG_PAR_DEFAUT = {"langue": "fr", "volume": 50}
def charger_config(chemin):
chemin = Path(chemin)
try:
texte = chemin.read_text(encoding="utf-8")
return json.loads(texte)
except FileNotFoundError:
print("Fichier absent, configuration par défaut.")
except json.JSONDecodeError:
print("Fichier corrompu, configuration par défaut.")
return dict(CONFIG_PAR_DEFAUT) # une copie, pour ne pas modifier l'original
# Tests
Path("bonne.json").write_text('{"langue": "en", "volume": 80}', encoding="utf-8")
Path("cassee.json").write_text('{"langue": "en", volume', encoding="utf-8")
print(charger_config("bonne.json"))
print(charger_config("absente.json"))
print(charger_config("cassee.json"))
```
```
{'langue': 'en', 'volume': 80}
Fichier absent, configuration par défaut.
{'langue': 'fr', 'volume': 50}
Fichier corrompu, configuration par défaut.
{'langue': 'fr', 'volume': 50}
```
On renvoie une **copie** (`dict(...)`) de la configuration par défaut : si l'appelant modifie le dictionnaire reçu, la constante globale reste intacte. `json.JSONDecodeError` est d'ailleurs une sous-classe de `ValueError`.
</details>
<details><summary>Corrigé de l'exercice 6</summary>
```python
def retirer_du_stock(stock, produit, quantite):
disponible = stock[produit] # KeyError si le produit n'existe pas
if quantite <= 0:
raise ValueError(f"quantité invalide : {quantite}")
if quantite > disponible:
raise ValueError(f"stock insuffisant pour {produit} ({disponible} disponible(s))")
stock[produit] = disponible - quantite
stock = {"pomme": 10, "poire": 4, "kiwi": 0}
demandes = [("pomme", 3), ("banane", 1), ("poire", 10), ("kiwi", -2), ("poire", 4)]
for produit, quantite in demandes:
try:
retirer_du_stock(stock, produit, quantite)
except KeyError:
print(f"Erreur : produit inconnu {produit!r}")
except ValueError as erreur:
print(f"Erreur : {erreur}")
else:
print(f"OK : {quantite} {produit}(s) retirée(s)")
print("Stock final :", stock)
```
```
OK : 3 pomme(s) retirée(s)
Erreur : produit inconnu 'banane'
Erreur : stock insuffisant pour poire (4 disponible(s))
Erreur : quantité invalide : -2
OK : 4 poire(s) retirée(s)
Stock final : {'pomme': 7, 'poire': 0, 'kiwi': 0}
```
La fonction ne fait **aucun** `print` : elle signale les problèmes par des exceptions, et c'est la boucle (l'appelant) qui décide comment les présenter. Le bloc `else` n'affiche « OK » que si aucune exception n'a eu lieu. `{produit!r}` affiche la chaîne avec ses guillemets.
</details>
<details><summary>Corrigé de l'exercice 7</summary>
```python
class MotDePasseInvalideError(Exception):
pass
def verifier_mot_de_passe(mdp):
if len(mdp) < 8:
raise MotDePasseInvalideError(
f"au moins 8 caractères (actuellement {len(mdp)})"
)
if not any(caractere.isdigit() for caractere in mdp):
raise MotDePasseInvalideError("il faut au moins un chiffre")
if not any(caractere.isupper() for caractere in mdp):
raise MotDePasseInvalideError("il faut au moins une majuscule")
return True
while True:
mdp = input("Mot de passe : ")
try:
verifier_mot_de_passe(mdp)
except MotDePasseInvalideError as erreur:
print(f"Refusé : {erreur}")
else:
print("Mot de passe accepté !")
break
```
Avec les entrées `abc`, `abcdefgh`, `abcdefg1`, `Abcdefg1` :
```
Mot de passe : abc
Refusé : au moins 8 caractères (actuellement 3)
Mot de passe : abcdefgh
Refusé : il faut au moins un chiffre
Mot de passe : abcdefg1
Refusé : il faut au moins une majuscule
Mot de passe : Abcdefg1
Mot de passe accepté !
```
`any(...)` renvoie `True` si au moins un élément est vrai (on a vu ce genre d'expression avec les compréhensions au [chapitre 06](06-chaines-et-comprehensions.md)). Une version sans `any` utiliserait une boucle `for` avec une variable booléenne.
</details>
<details><summary>Corrigé de l'exercice 8</summary>
Les trois bugs :
1. **Plantage** : `top_mots(...)[3]` provoque `IndexError: list index out of range`, car la liste du top 3 a les indices 0, 1, 2. De plus, on veut le **mot** (premier élément de la paire), donc `[0][0]`. *Trouvé grâce au traceback.*
2. **Le compteur ne compte pas** : dans `compter_mots`, les deux branches font `= 1`. Il faut `compteur[mot] += 1` dans la première. *Trouvé avec un `print(compteur)` ou en observant `compteur` dans le panneau VARIABLES : tous les mots valaient 1.*
3. **Tri à l'envers** : `sorted` trie par ordre croissant, donc on obtient les mots les **moins** fréquents. Il faut `reverse=True`. *Trouvé en affichant `classement`.*
```python
def compter_mots(texte):
compteur = {}
for mot in texte.split():
mot = mot.lower().strip(".,!?")
if mot in compteur:
compteur[mot] += 1 # bug 2 corrigé
else:
compteur[mot] = 1
return compteur
def top_mots(compteur, n):
classement = sorted(compteur.items(), key=lambda paire: paire[1], reverse=True) # bug 3
return classement[:n]
texte = "Le chat dort. Le chien dort aussi. Le chat rêve !"
for mot, nombre in top_mots(compter_mots(texte), 3):
print(f"{mot} : {nombre}")
print("Mot le plus fréquent :", top_mots(compter_mots(texte), 3)[0][0]) # bug 1
```
```
le : 3
chat : 2
dort : 2
Mot le plus fréquent : le
```
Attention à un détail : le mot `!` isolé à la fin devient la chaîne vide `""` après `strip`. Il n'apparaît pas dans le top 3 ici, mais une version plus propre ignorerait les mots vides avec `if not mot: continue`. Remarque aussi que `sorted` est **stable** : à égalité (chat et dort valent 2), l'ordre d'apparition est conservé.
</details>
<details><summary>Corrigé du mini-projet</summary>
```python
"""Calculatrice en ligne de commande qui ne plante jamais."""
class ExpressionInvalideError(Exception):
pass
OPERATEURS = ["+", "-", "*", "/", "//", "%", "**"]
AIDE = """Saisis : nombre opérateur nombre (séparés par des espaces).
Opérateurs : + - * / // % **
'ans' = dernier résultat. Commandes : historique, aide, quitter."""
def convertir_nombre(texte, dernier_resultat):
"""Convertit un texte en float. Accepte 'ans' et la virgule décimale."""
if texte.lower() == "ans":
return dernier_resultat
try:
return float(texte.replace(",", "."))
except ValueError:
raise ExpressionInvalideError(f"{texte!r} n'est pas un nombre.") from None
def calculer(a, operateur, b):
"""Effectue le calcul. Peut lever ZeroDivisionError ou OverflowError."""
match operateur:
case "+":
return a + b
case "-":
return a - b
case "*":
return a * b
case "/":
return a / b
case "//":
return a // b
case "%":
return a % b
case "**":
return a**b
case _:
raise ExpressionInvalideError(f"opérateur inconnu {operateur!r}.")
def analyser(ligne, dernier_resultat):
"""Découpe la ligne en (a, operateur, b)."""
morceaux = ligne.split()
if len(morceaux) != 3:
raise ExpressionInvalideError(
"il faut 3 éléments séparés par des espaces (ex : 3 + 4)."
)
texte_a, operateur, texte_b = morceaux
a = convertir_nombre(texte_a, dernier_resultat)
b = convertir_nombre(texte_b, dernier_resultat)
return a, operateur, b
def formater(nombre):
"""Affiche 16 plutôt que 16.0, et arrondit l'affichage des décimaux."""
if isinstance(nombre, complex):
return str(nombre)
if nombre.is_integer():
return str(int(nombre))
return f"{nombre:.10g}"
def boucle_principale():
historique = []
dernier_resultat = 0.0
print("Calculatrice robuste — tape 'aide' pour l'aide, 'quitter' pour sortir.")
while True:
ligne = input("> ").strip()
commande = ligne.lower()
if commande == "":
continue
if commande == "quitter":
break
if commande == "aide":
print(AIDE)
continue
if commande == "historique":
if not historique:
print("(historique vide)")
for numero, calcul in enumerate(historique, start=1):
print(f"{numero}. {calcul}")
continue
try:
a, operateur, b = analyser(ligne, dernier_resultat)
resultat = calculer(a, operateur, b)
except ExpressionInvalideError as erreur:
print(f"Erreur : {erreur}")
except ZeroDivisionError:
print("Erreur : division par zéro.")
except OverflowError:
print("Erreur : résultat trop grand.")
else:
dernier_resultat = resultat
texte = f"{formater(a)} {operateur} {formater(b)} = {formater(resultat)}"
historique.append(texte)
print(f"= {formater(resultat)}")
def main():
try:
boucle_principale()
except (KeyboardInterrupt, EOFError):
print() # passe à la ligne après le ^C ou ^D
print("Au revoir !")
main()
```
Session de test (les saisies sont après `> `) :
```
Calculatrice robuste — tape 'aide' pour l'aide, 'quitter' pour sortir.
> 12 + 4
= 16
> ans * 2,5
= 40
> 10 / 0
Erreur : division par zéro.
> 3 $ 4
Erreur : opérateur inconnu '$'.
> douze + 1
Erreur : 'douze' n'est pas un nombre.
> 5 +
Erreur : il faut 3 éléments séparés par des espaces (ex : 3 + 4).
> 2.0 ** 10000
Erreur : résultat trop grand.
> 7 // 2
= 3
> 0.1 + 0.2
= 0.3
> historique
1. 12 + 4 = 16
2. 16 * 2.5 = 40
3. 7 // 2 = 3
4. 0.1 + 0.2 = 0.3
> quitter
Au revoir !
```
Points importants :
- **Une seule exception personnalisée** pour toutes les erreurs de saisie : la boucle principale n'a pas besoin de savoir *quel* contrôle a échoué, le message le dit.
- `raise ... from None` dans `convertir_nombre` masque l'exception d'origine (`ValueError`) dans un éventuel traceback : on la remplace volontairement par la nôtre.
- Le `try` principal ne contient que les deux appels risqués ; la mise à jour de l'historique est dans le `else`.
- `-8 ** 0.5` (la puissance d'un nombre négatif par un décimal) donne un nombre **complexe** en Python (`(1.7319121124709868e-16+2.8284271247461903j)`) : `formater` le gère avec `isinstance` au lieu de planter sur `.is_integer()`. Ce genre de cas limite, on le découvre… en essayant de faire planter son programme !
- `f"{nombre:.10g}"` affiche au plus 10 chiffres significatifs : `0.1 + 0.2` s'affiche `0.3` au lieu de `0.30000000000000004`.
- Ctrl+C (`KeyboardInterrupt`) et Ctrl+D / fin de fichier (`EOFError`) sont attrapés **une seule fois**, autour de toute la boucle.
</details>
---
## Récapitulatif
- Une **erreur de syntaxe** empêche le programme de démarrer ; une **exception** survient pendant l'exécution.
- Un **traceback se lit de bas en haut** : dernière ligne = *quoi* ; dernier bloc `File` de ton fichier = *où* ; blocs au-dessus = *comment on en est arrivé là*.
- Exceptions courantes : `ValueError` (mauvaise valeur), `TypeError` (mauvais type), `KeyError` (clé absente), `IndexError` (indice hors limites), `FileNotFoundError` (fichier introuvable), `ZeroDivisionError`, `NameError`, `AttributeError`. Elles forment une **hiérarchie** : attraper un parent attrape ses enfants.
- `try` / `except Type` intercepte une erreur précise ; `as erreur` récupère l'objet et son message ; `else` s'exécute si tout s'est bien passé ; `finally` s'exécute toujours.
- **Jamais de `except:` nu** ; garde le bloc `try` le plus court possible ; exceptions précises d'abord.
- `raise ValueError("message")` signale un problème et laisse l'appelant décider ; `raise` seul relance ; `raise ... from ...` chaîne les erreurs.
- Créer une exception : `class MonErreur(Exception): pass` (les classes seront expliquées au chapitre 10).
- Déboguer : `print(f"{variable=}")`, `breakpoint()` (commandes `p`, `n`, `s`, `l`, `c`, `q`), débogueur VS Code (point d'arrêt F9, lancement F5, pas à pas F10 / F11, panneaux Variables / Watch / Call Stack).
- `assert condition, "message"` vérifie tes hypothèses de programmeur et sert aux tests, **pas** à valider des saisies.
### Checklist de validation
- [ ] Je sais trouver en moins de 30 secondes le type d'erreur et la ligne fautive dans un traceback.
- [ ] Je sais citer et expliquer au moins six exceptions courantes.
- [ ] Je sais écrire une boucle qui redemande une saisie jusqu'à ce qu'elle soit valide.
- [ ] Je sais à quoi servent `else` et `finally` dans un `try`.
- [ ] Je sais lever une exception avec `raise` et créer ma propre exception.
- [ ] J'ai posé un point d'arrêt dans VS Code et avancé pas à pas en observant les variables.
- [ ] Je sais pourquoi `except:` tout seul est dangereux et pourquoi `assert` ne doit pas valider une saisie.
- [ ] Ma calculatrice résiste à toutes les saisies que j'ai pu imaginer, y compris Ctrl+C.
---
[← Chapitre précédent](07-fichiers-csv-json.md) | [Sommaire](README.md) | [Chapitre suivant →](09-modules-bibliotheque-standard.md)