Leçon 05-3 — Exceptions : try/except/else/finally et raise
1. Objectif
À la fin de cette leçon tu sauras : intercepter une erreur précise avec try/except au lieu de laisser le programme planter, utiliser plusieurs except, else et finally à bon escient, lever toi-même une exception avec raise ValueError(...) quand une situation est anormale, valider proprement une saisie utilisateur, et expliquer pourquoi except: nu est interdit dans ce cours — et dans tout code sérieux.
2. Pourquoi c'est important
Depuis le niveau 01 tu lis des tracebacks ; il est temps de passer de l'autre côté. Un vrai programme vit dans un monde hostile : l'utilisateur tape douze au lieu de 12, le fichier a été supprimé, le disque est plein. Sans gestion d'erreurs, chaque imprévu tue le programme avec un traceback — acceptable pour un exercice, inacceptable pour un outil (imagine ta todo-list qui crashe ET perd tes données parce que tu as tapé un espace de trop). Les exceptions sont LE mécanisme Python pour dire : « ici, telle erreur précise peut arriver, et voici quoi faire ». Bien utilisé, il rend tes programmes indestructibles ; mal utilisé (except: nu), il cache tes bugs au lieu de les gérer — cette leçon t'apprend la différence.
3. Explication simple
Une exception, c'est le signal d'alarme que Python déclenche quand quelque chose d'impossible est demandé. Tu les connais déjà de vue : ValueError, TypeError, FileNotFoundError, KeyError, ZeroDivisionError… Par défaut, l'alarme arrête tout : c'est le traceback.
try/except te laisse poser un filet sous un morceau de code :
try:
age = int(input("Ton âge : ")) # peut déclencher ValueError
except ValueError:
print("Ce n'est pas un nombre entier.")
Se lit : « essaie ce bloc ; SI une ValueError éclate dedans, ne plante pas — exécute le bloc except à la place ». Deux issues possibles :
- la saisie est
42→int()réussit, leexceptest ignoré ; - la saisie est
douze→int()lèveValueError, Python saute immédiatement au blocexcept, le programme continue.
Le filet est sélectif : except ValueError n'attrape QUE les ValueError. Une autre erreur (un NameError dû à une faute de frappe, par exemple) passe à travers et plante — et c'est voulu : cette erreur-là est un bug à corriger, pas un imprévu à gérer.
4. Explication approfondie
a) L'anatomie complète : try/except/else/finally.
try:
number = int(text) # 1. le code SURVEILLÉ — le minimum possible
except ValueError:
print("pas un nombre") # 2. si (et seulement si) ValueError a éclaté
else:
print(number * 2) # 3. si AUCUNE exception : la suite normale
finally:
print("terminé") # 4. TOUJOURS exécuté, erreur ou pas
else répond à une vraie question de design : où mettre le code qui doit tourner seulement si tout s'est bien passé ? Si tu le mets dans le try, ses propres erreurs seraient attrapées par ton except — confusion garantie. Dans le else, il est protégé de rien mais conditionné au succès. finally sert au nettoyage inconditionnel (fermer une connexion, effacer un fichier temporaire). Tu utilises finally sans le savoir depuis la leçon 05-1 : with est exactement un try/finally qui ferme le fichier.
b) La règle des exceptions PRÉCISES — la règle la plus importante du niveau. Écris toujours l'exception la plus précise possible, et jamais except: nu :
except ValueError: # ✅ précis : je gère CE cas prévu
except (ValueError, KeyError): # ✅ plusieurs types, même traitement
except: # ❌ INTERDIT — attrape TOUT, aveuglément
Pourquoi c'est une faute et pas une préférence de style : except: nu attrape toutes les exceptions — y compris tes fautes de frappe (NameError), tes bugs de types (TypeError), et même le Ctrl+C de l'utilisateur (KeyboardInterrupt) qui devient incapable d'arrêter ton programme. Le symptôme : un programme qui « marche » en affichant des résultats faux, avec un bug enterré vivant quelque part. Un traceback est une information ; except: nu est un broyeur d'informations. (Son cousin except Exception: attrape un peu moins large — il laisse passer Ctrl+C — mais reste un dernier recours qu'on n'utilise qu'avec de bonnes raisons, pas un réflexe.)
c) Le try minimal. Même logique : le bloc try doit entourer la ou les lignes qui peuvent légitimement échouer, pas trente lignes. Un try géant, c'est ne plus savoir quelle ligne a levé quoi — et attraper par accident des erreurs qu'on ne visait pas. Motif : isole l'opération risquée (la conversion, l'ouverture de fichier), gère-la, et laisse le reste respirer hors du try.
d) raise : déclencher l'alarme toi-même. Les exceptions ne sont pas réservées à Python — quand TON code détecte une situation anormale, il la signale pareil :
def parse_age(text):
age = int(text) # peut lever ValueError (tant mieux)
if age < 0 or age > 130:
raise ValueError(f"age out of range: {age}")
return age
raise ValueError("message") fabrique l'exception et la lance ; l'exécution de la fonction s'arrête net (comme un return, mais par la sortie de secours). L'appelant peut l'attraper avec le même try/except que pour int(). C'est la division du travail propre : la fonction détecte et signale, l'appelant décide quoi faire (redemander, afficher, abandonner). Une fonction qui fait print("erreur") et return None mélange les deux rôles — l'appelant ne peut plus distinguer une vraie valeur d'un échec.
Difficulté honnête : la syntaxe s'apprend en dix minutes. Ce qui demande des semaines, c'est le jugement — QUOI attraper (les erreurs prévues : saisie invalide, fichier absent), OÙ (au plus près de qui sait réagir), et quoi laisser planter (les bugs : ils doivent faire du bruit pour être corrigés). Les exercices de cette leçon travaillent exactement ce jugement ; attends-toi à hésiter, c'est le but.
5. Exemples commentés
Exemple 1 — le filet de base
text = input("Un nombre entier : ")
try:
number = int(text)
print(f"Le double vaut {number * 2}")
except ValueError:
print(f"'{text}' n'est pas un nombre entier.")
Sortie avec 21 : Le double vaut 42. Avec vingt-et-un : 'vingt-et-un' n'est pas un nombre entier. — pas de traceback, le programme garde la main et peut continuer.
Exemple 2 — LE motif à mémoriser : redemander jusqu'à une saisie valide
while True: # boucle infinie... jusqu'au break
try:
age = int(input("Ton âge : "))
break # conversion réussie : on sort
except ValueError:
print("Entier attendu, réessaie.")
print(f"Âge enregistré : {age}")
Session :
Ton âge : douze
Entier attendu, réessaie.
Ton âge : 12.5
Entier attendu, réessaie.
Ton âge : 12
Âge enregistré : 12
while True + try + break : le motif standard de validation de saisie. Tu l'écrivais au niveau 02 avec des tests isdigit() fragiles ; la version exception est plus simple ET plus complète (nombres négatifs, espaces…).
Exemple 3 — plusieurs except : un traitement par type d'erreur
filename = input("Fichier à afficher : ")
try:
with open(filename, encoding="utf-8") as f:
content = f.read()
except FileNotFoundError:
print(f"'{filename}' n'existe pas (dossier courant : à vérifier ?).")
except PermissionError:
print(f"'{filename}' n'est pas lisible (c'est un dossier ?).")
except UnicodeDecodeError:
print(f"'{filename}' n'est pas un fichier texte en utf-8.")
else:
print(content)
Python essaie les except dans l'ordre et exécute le premier qui correspond. Trois pannes différentes, trois messages utiles — compare avec un except: nu qui afficherait « erreur » pour les trois ET pour n'importe quel bug. Note le else : l'affichage n'a lieu que si la lecture a réussi.
Exemple 4 — else et finally : qui s'exécute quand ?
def demo(value):
print(f"--- demo({value!r})")
try:
number = int(value)
except ValueError:
print("except: not a number")
else:
print("else: got", number)
finally:
print("finally: always")
demo("12")
demo("abc")
Sortie (vérifiée) :
--- demo('12')
else: got 12
finally: always
--- demo('abc')
except: not a number
finally: always
else OU except — jamais les deux ; finally — toujours. Relis cette sortie jusqu'à pouvoir la prédire.
Exemple 5 — raise : la fonction signale, l'appelant gère
def parse_age(text):
"""Return the age as an int, raise ValueError if invalid."""
age = int(text) # ValueError possible : on la LAISSE monter
if age < 0 or age > 130:
raise ValueError(f"age out of range: {age}")
return age
while True:
try:
age = parse_age(input("Âge : "))
break
except ValueError:
print("Âge invalide, réessaie.")
print(f"OK : {age} ans")
parse_age a UNE responsabilité : valider et convertir. Elle ne fait pas de print, ne redemande pas — elle renvoie ou elle lève. La boucle de saisie, elle, attrape ValueError d'où qu'elle vienne : de int() (saisie douze) ou de notre raise (saisie -5). Un seul filet pour les deux cas, c'est exactement pourquoi on a choisi ValueError et pas une autre.
Exemple 6 — gérer le premier lancement d'un programme à fichier
from pathlib import Path
SAVE_FILE = Path("courses.txt")
def load_items():
"""Return the saved list, or an empty list on first run."""
try:
with open(SAVE_FILE, encoding="utf-8") as f:
items = []
for line in f:
items.append(line.strip())
return items
except FileNotFoundError:
return [] # first run: no file yet, empty list
items = load_items()
print(f"{len(items)} article(s) chargé(s)")
LE cas d'école du niveau : au premier lancement le fichier n'existe pas, et ce n'est pas une erreur — c'est un cas prévu, qui a une réponse propre (liste vide). try/except FileNotFoundError exprime ça en quatre lignes. C'est la pièce qui manquait à ton compteur et à ton journal de 05-1.
6. Erreurs fréquentes
1) except: nu — la faute capitale
try:
totale = totale + int(input("Nombre : "))
except: # ❌ attrape AUSSI le NameError !
print("Saisie invalide.")
Il y a un bug ici : totale n'existe pas (faute de frappe pour total). Le NameError est avalé, le programme répète « Saisie invalide » quoi qu'on tape — le bug est invisible, le message est mensonger. Avec except ValueError:, le NameError aurait planté avec un traceback clair dès le premier essai. Règle du cours : toujours nommer l'exception.
2) Le try fourre-tout
try:
filename = input("Fichier : ")
with open(filename, encoding="utf-8") as f:
lines = f.readlines()
total = 0
for line in lines:
total += int(line)
print(f"Moyenne : {total / len(lines)}")
except ValueError:
print("Fichier introuvable ?")
Trente problèmes en un : le message ment (ValueError vient de int(line), pas d'un fichier introuvable), le FileNotFoundError réel n'est pas géré, et la division par zéro (fichier vide) non plus. Correction : UN try de deux lignes autour de l'ouverture (→ FileNotFoundError), UN autre autour de la conversion — chaque risque son filet, chaque message sa vérité.
3) Attraper trop tôt — et renvoyer n'importe quoi
def parse_age(text):
try:
return int(text)
except ValueError:
return 0 # ❌ 0 est un âge "valide"... le mensonge se propage
L'appelant reçoit 0 et ne saura jamais que la saisie était invalide — le bug ressortira plus loin, déguisé (statistiques faussées, par exemple). Correction : ne PAS attraper ici — laisser la ValueError monter vers la boucle de saisie, qui sait quoi faire (redemander). Attraper une exception qu'on ne sait pas traiter, c'est juste déplacer le problème en le maquillant.
4) Croire que le try protège les lignes suivantes
try:
number = int(input("Nombre : "))
except ValueError:
print("Invalide.")
print(number * 2) # NameError si la saisie était invalide !
Si int() échoue, number n'a jamais été créée — et la dernière ligne plante. Le except gère l'erreur mais le programme continue APRÈS lui. Corrections possibles : la boucle de l'exemple 2 (on ne sort qu'avec un number valide), ou tout le code dépendant dans le else.
5) raise sans message
raise ValueError # légal mais mesquin
raise ValueError(f"age out of range: {age}") # ✅ le futur toi te remercie
Le message de l'exception est ce qui s'affiche dans le traceback et ce que l'appelant peut journaliser. ValueError sans contexte oblige à ouvrir le code pour comprendre ; avec la valeur fautive dedans, le diagnostic est immédiat. Mets la valeur reçue dans chaque message de raise.
6) Tester les exceptions avec if d'abord (le réflexe LBYL)
if filename_exists_somehow(...): # vérifier AVANT... comment, d'ailleurs ?
f = open(filename)
En Python, l'idiome est inverse : essaie, et gère l'échec (« EAFP : Easier to Ask Forgiveness than Permission »). Entre ton test exists() et ton open(), le fichier peut disparaître ; et beaucoup d'échecs (encodage, permissions) ne se testent pas à l'avance. exists() (05-2) sert à la logique métier (« premier lancement ? ») — pour se protéger d'un échec, le try/except est l'outil.
7. Lire les messages d'erreur
Le traceback d'un raise maison, à deux étages :
Traceback (most recent call last):
File "C:\Users\jtron\Claude\learnpython\lessons\level-05-fichiers-exceptions-modules\exercices\mes-reponses\age.py", line 7, in <module>
parse_age("-5")
~~~~~~~~~^^^^^^
File "C:\Users\jtron\Claude\learnpython\lessons\level-05-fichiers-exceptions-modules\exercices\mes-reponses\age.py", line 4, in parse_age
raise ValueError(f"age out of range: {age}")
ValueError: age out of range: -5
Lecture de bas en haut :
- Dernière ligne :
ValueError: age out of range: -5— et tu vois immédiatement la récompense du message soigné : la valeur fautive est sous tes yeux. - Étage du dessus : ligne 4, dans
parse_age— la ligne duraise, là où l'anomalie a été DÉTECTÉE. - Étage encore au-dessus : ligne 7, dans
<module>— l'appelparse_age("-5"), là d'où venait la valeur.
Un traceback se lit comme une remontée de filière : la dernière ligne dit QUOI, et chaque étage dit PAR OÙ c'est passé. Avec raise, voir sa propre ligne dans un traceback est normal et sain — l'exception a fait son travail : signaler fort, avec contexte.
Dernier point, capital : quand une exception est attrapée, il n'y a AUCUN traceback — c'est tout l'intérêt. Si tu veux vérifier qu'un except s'exécute vraiment, mets-y un print temporaire. Et si un traceback t'annonce une exception que tu croyais attraper, vérifie le TYPE exact dans sa dernière ligne : attraper ValueError ne stoppe pas un TypeError.
8. Exercices — faciles
Règles : fichiers dans
lessons/level-05-fichiers-exceptions-modules/exercices/mes-reponses/, nommésex-05-3-a.py, etc. Pour chaque programme : provoque VOLONTAIREMENT chaque erreur gérée, pour voir tonexcepten action.
a) Écris le motif de saisie robuste : demande un nombre entier en boucle jusqu'à obtenir une saisie valide, puis affiche son carré. Teste avec douze, 3.5, 7 (avec espaces), 7.
b) Sans exécuter : note sur papier ce qu'affiche demo("3") puis demo("trois") puis demo(" 3 ") avec la fonction demo de l'exemple 4. Vérifie ensuite en la recopiant. (Le troisième cas est moins évident qu'il n'en a l'air — int tolère-t-il les espaces ?)
c) Reprends ton compteur.py de la leçon 05-1 et gère le premier lancement : si compteur.txt n'existe pas, le compteur démarre à 0 au lieu de planter. Supprime le fichier et vérifie. Contrainte : le try n'entoure QUE la lecture.
9. Exercices — moyens
a) Écris affiche_fichier.py : demande un nom de fichier et affiche son contenu, avec trois messages distincts pour FileNotFoundError, PermissionError et UnicodeDecodeError, et l'affichage du contenu dans un else. Provoque les trois : nom bidon, nom d'un DOSSIER, et un fichier cp1252 fabriqué avec Path("vieux.txt").write_text("café", encoding="cp1252").
b) Écris parse_price(text) : renvoie le prix en float, accepte la virgule française ("12,50" → 12.5), et lève une ValueError avec un message incluant la saisie si le texte n'est pas un prix ou s'il est négatif. Puis une boucle de saisie qui l'utilise. Interdits : print dans parse_price, except dans parse_price. (Indice : text.replace(",", ".") ⏪ niveau 03, puis laisse float() faire la police.)
c) Le code suivant contient un bug caché par un except: nu — recopie-le tel quel, observe son comportement, trouve le bug SANS le corriger d'abord (en remplaçant le except par une version précise), puis livre la version propre :
def average(values):
try:
totl = sum(values)
return totl / len(values)
except:
return 0
print(average([10, 20, 30])) # 20.0 attendu
print(average([])) # et là, que DOIT-il se passer ?
Question incluse dans l'exercice : pour une liste vide, faut-il renvoyer 0, ou lever une exception ? Justifie en commentaire.
10. Exercices — difficiles
a) La liste de courses indestructible. Reprends la liste de courses du niveau 03 (menu while : ajouter / retirer / afficher / quitter) et rends-la persistante ET blindée : chargement depuis courses.txt avec liste vide au premier lancement ; sauvegarde après CHAQUE modification ; aucune saisie ne doit pouvoir la faire planter (numéro hors liste, texte au lieu d'un nombre, ligne vide…). Teste en essayant sincèrement de la casser.
- Indice 1 : trois fonctions —
load_items(),save_items(items), et le menu. Une seule attrapeFileNotFoundError: laquelle ? - Indice 2 : « retirer » a DEUX validations distinctes : la saisie est-elle un entier (
ValueError), et l'indice est-il dans la liste ? Pour la seconde, unifsuffit — toutes les vérifications ne méritent pas une exception. - Indice 3 : le motif de l'exemple 2 pour la saisie du numéro, celui de l'exemple 6 pour le chargement ; la sauvegarde, elle, n'a rien à attraper.
11. Mini-projet lié — « gestionnaire de tâches, v2 persistante »
Le projet complet est spécifié dans projects/mini-projects/06-gestionnaire-de-taches.md : une todo-list qui survit au redémarrage. Si tu as fait sa v1 au niveau 03, c'est l'heure de la v2 (étapes 6 à 8 de la fiche, ~1 h 30) : chargement au démarrage avec try/except FileNotFoundError, sauvegarde après chaque modification, validation de toutes les saisies. La fiche utilise le format JSON (json.load/json.dump, guidés pas à pas dedans — ⏩ le module json est approfondi au niveau 09 ; l'import nécessaire est l'objet de la leçon 05-4, tu peux aussi faire une première version en fichier texte simple comme l'exercice difficile, puis passer au JSON après 05-4).
Son extension 4 — gérer un fichier de sauvegarde corrompu sans perdre de données — est exactement l'esprit de cette leçon : un cas prévu, un filet précis, une réponse digne.
12. Correction / méthode de correction
Les solutions détaillées sont dans solutions/serie-05-3-solutions.md.
Méthode : (1) finis TOUS les exercices avant d'ouvrir les solutions ; (2) compare le raisonnement — surtout les DÉCISIONS : qu'as-tu attrapé, où, et qu'as-tu laissé planter ? C'est là que se joue la leçon, pas dans la syntaxe ; (3) vérifie qu'aucun de tes except n'est nu ni ne masque un cas non prévu ; (4) une ligne de journal par écart ; (5) refais de tête le lendemain.
13. À retenir
try/except TypePrécis: le filet sous les lignes qui peuvent légitimement échouer — et SEULEMENT elles.except:nu est interdit : il avale les bugs, les vrais messages et Ctrl+C. Toujours nommer l'exception.- Plusieurs
except= un traitement par panne ;else= la suite si tout va bien ;finally= toujours (c'est le moteur dewith). - Le motif de saisie :
while True:+try:+breakdans letry. raise ValueError(f"... {valeur}"): ta fonction détecte et signale, l'appelant décide. Jamais deprint-et-return Nonepour signaler une erreur.FileNotFoundErrorau premier lancement n'est pas un bug : c'est un cas prévu → valeur par défaut.- Attraper une erreur qu'on ne sait pas traiter = maquiller le problème. Un bug doit faire du bruit.
- Exception attrapée = pas de traceback ; traceback = remontée de filière, à lire de bas en haut.
14. Questions de révision
- Pourquoi
except:nu est-il une faute, en trois arguments concrets ? - Dans
try/except/else/finally: qu'est-ce qui s'exécute quand tout va bien ? Quand l'exception attendue éclate ? Quand une exception NON attendue éclate ? - Pourquoi met-on le moins de lignes possible dans le
try? - Récite le motif de validation de saisie (les quatre lignes). Pourquoi le
breakest-il DANS letry? - Ta fonction détecte une valeur aberrante. Pourquoi
raise ValueError(...)plutôt quereturn Noneou unprint? - Quel
exceptgère proprement le premier lancement d'un programme à fichier de sauvegarde, et que fait-on dedans ? - Que devient un
TypeErrorlevé dans un bloc protégé parexcept ValueError? - Que signifie « EAFP », et pourquoi
if path.exists():avantopen()n'est-il pas une protection suffisante ?
15. Checklist de compréhension
- Je sais écrire le motif de saisie robuste sans modèle.
- Je peux prédire la sortie exacte de l'exemple 4 pour une valeur valide et une invalide.
- Aucun de mes exercices ne contient
except:nu — vérifié. - Je sais écrire une fonction qui
raise ValueErroravec un message incluant la valeur fautive, et l'attraper depuis l'appelant. - Je sais gérer un fichier de sauvegarde absent sans planter NI masquer les autres erreurs.
- Je peux expliquer à voix haute la différence entre « gérer une erreur prévue » et « cacher un bug ».
- Ma liste de courses de l'exercice difficile a résisté à mes tentatives de la casser.
Si une case reste vide : refais les exercices de la section correspondante demain, AVANT d'ouvrir la leçon suivante.
16. Commit conseillé
git add lessons/level-05-fichiers-exceptions-modules/exercices/mes-reponses/
git commit -m "exercises: complete lesson 05-3 exceptions try except raise"
Puis, si c'est ta fin de session : entrée de journal + git push.