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 (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 avectry/except/else/finallysans masquer les vrais bugs ; - lever tes propres erreurs avecraiseet créer tes propres types d'exceptions ; - déboguer un programme avecbreakpoint()et le débogueur de VS Code (points d'arrêt, pas à pas) ; - utiliserassertpour 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 :
- 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. - 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.
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 :
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 :
- 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. - Juste au-dessus, le dernier bloc
File ...indique où l'erreur s'est produite : fichiermon_script.py, ligne 3, dans la fonctioncalculer_moyenne, sur l'instructionreturn total / len(notes). Les petits~~~~^~~~soulignent l'opération exacte qui a échoué : la division/. - En remontant encore, tu vois qui a appelé cette fonction :
afficher_bilan, ligne 7. - 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'appelafficher_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
Filepointent 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¶
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¶
"Â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¶
Le message affiche la clé fautive. Pense à prix.get("poire") ou au test if "poire" in prix.
IndexError — indice hors de la liste¶
Une liste de 3 éléments a les indices 0, 1 et 2. L'erreur classique « décalée de un ».
FileNotFoundError — fichier introuvable¶
Soit le nom est faux, soit tu n'exécutes pas le script depuis le bon dossier (revois le chapitre 07 sur les chemins).
ZeroDivisionError — division par zéro¶
Vaut aussi pour // et %.
Deux autres très fréquentes¶
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 :
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 ».
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 :
Avec l'entrée 20 :
Le déroulement :
- Python exécute le bloc
tryligne par ligne. - Si aucune erreur ne survient, le bloc
exceptest ignoré. - Si une
ValueErrorsurvient, Python abandonne le reste du bloctry(leprintn'est pas exécuté) et saute dansexcept 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 :
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 :
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) :
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 LookupErroravantexcept 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 :
try:
int("abc")
except ValueError as erreur:
print("Type :", type(erreur).__name__)
print("Message :", erreur)
C'est pratique pour informer l'utilisateur sans lui montrer un traceback complet :
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}")
6. Les blocs else et finally¶
La structure complète ressemble à ceci :
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.
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)
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…).
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))
💡 Pour les fichiers,
with open(...)(vu au chapitre 07) fait déjà le ménage à ta place : il ferme le fichier même en cas d'erreur. C'est unfinallyinté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.
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 :
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 :
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.")
Tu peux aussi transformer une erreur en une autre plus parlante, en gardant la trace de l'originale avec from :
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 ; pour l'instant, retiens simplement cette syntaxe à apprendre par cœur :
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 :
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} €")
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 :
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
ifavant 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.
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é :
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} €")
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.
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 :
remise=1.0pour 19,99 € au lieu de 1,999 : c'est//(division entière) au lieu de/.totalne s'accumule pas : il est remplacé à chaque tour (total =au lieu detotal +=).
Astuce : la f-string avec = affiche le nom et la valeur, idéal pour déboguer :
(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.)
⚠️ Pense à retirer tes
[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 :
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 |
print : affiche la valeur d'une expression |
n |
next : exécute la ligne courante et passe à la suivante (sans entrer dans les fonctions appelées) |
s |
step : entre dans la fonction appelée sur la ligne courante |
l |
list : affiche le code autour de la ligne courante |
c |
continue : reprend l'exécution normale jusqu'au prochain breakpoint() |
q |
quit : 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) 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
totalouprix * 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
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 :
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 :
def inverser(texte):
return texte[::-1]
assert inverser("abc") == "cba"
assert inverser("") == ""
assert inverser("été") == "été"
print("Tous les tests passent.")
C'est d'ailleurs exactement ce qu'utilise l'outil de test pytest que tu découvriras au chapitre 13.
⚠️ Ne valide jamais une saisie utilisateur avec
assert. Python peut être lancé avec l'option-O(optimisation) qui désactive tous lesassert. Pour une donnée venant de l'extérieur (saisie, fichier), utiliseif+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 :
- Reproduire : trouve les entrées exactes qui provoquent le bug. Un bug qu'on ne sait pas reproduire ne se corrige pas.
- Lire : s'il y a un traceback, applique la méthode des 3 questions (quoi, où, pourquoi).
- Isoler : réduis le problème. Commente des morceaux, teste la fonction suspecte seule avec des valeurs simples.
- Observer :
print,breakpoint()ou VS Code pour comparer ce que tu crois que contiennent tes variables avec ce qu'elles contiennent vraiment. - Corriger puis vérifier : corrige une seule chose à la fois, relance, et ajoute un
assertpour 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¶
# ❌ À 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.
# ✅ 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¶
except ValueError, TypeError:
^^^^^^^^^^^^^^^^^^^^^
SyntaxError: multiple exception types must be parenthesized
✅ Correction : except (ValueError, TypeError):.
Piège 3 — Un try trop large¶
# ❌ 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")
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.
# ✅ 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¶
donnees = {"a": 1}
try:
donnees["b"]
except LookupError:
print("Problème de recherche")
except KeyError:
print("Clé absente") # ❌ jamais atteint
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¶
On ne peut lever que des objets exception. ✅ raise ValueError("Erreur grave").
Piège 6 — Valider des données avec assert¶
# ❌ 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 :
Piège 7 — Oublier de convertir la saisie (et croire à un bug mystérieux)¶
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¶
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.
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.
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 ?
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 enfloat;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").
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 affichantFichier 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 affichantFichier 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 :
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.
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 :
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.
- Le programme affiche une invite
>et lit une expression de la formenombre opérateur nombre, séparée par des espaces :12 + 4,7.5 * 2,2 ** 10. - Opérateurs acceptés :
+,-,*,/,//,%,**. - Les nombres peuvent être décimaux, avec un point ou une virgule (
3,5). - Le résultat est affiché ; s'il est entier (par exemple
4.0), on l'affiche sans décimale (4). - Le mot-clé
anspeut remplacer un nombre : il vaut le dernier résultat calculé (0 au départ). - Commandes spéciales :
historique(affiche tous les calculs réussis),aide,quitter. - 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 ** 10000enfloat→OverflowError). - Ctrl+C et Ctrl+D (fin de saisie) quittent proprement avec
Au revoir !, sans traceback. - Le code définit une exception personnalisée
ExpressionInvalideErrorpour 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¶
- Écris
convertir_nombre(texte, dernier_resultat): gèreans, remplace,par., appellefloat()et transforme laValueErrorenExpressionInvalideErroravec un message clair. - Écris
calculer(a, operateur, b): un dictionnaire ou unmatchqui associe chaque opérateur à son calcul. LèveExpressionInvalideErrorsi l'opérateur est inconnu. LaisseZeroDivisionErroretOverflowErrorse produire naturellement. - Écris
analyser(ligne, dernier_resultat)qui découpe la ligne, vérifie qu'il y a 3 morceaux et renvoie(a, operateur, b). - Écris
formater(nombre)qui affiche16plutôt que16.0. - Écris la boucle principale avec un seul
tryqui attrape chaque exception séparément, avec le bon message. - Entoure le tout d'un
try/except (KeyboardInterrupt, EOFError). - 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¶
Corrigé de l'exercice 1
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.Corrigé de l'exercice 2
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`.Corrigé de l'exercice 3
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` : Leçon : le crime a eu lieu ligne 2, mais le coupable était dans les **données** de la ligne 10.Corrigé de l'exercice 4
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)
Corrigé de l'exercice 5
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"))
Corrigé de l'exercice 6
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)
Corrigé de l'exercice 7
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
Corrigé de l'exercice 8
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`.*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
Corrigé du mini-projet
"""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()
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 !
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
Filede 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 Typeintercepte une erreur précise ;as erreurrécupère l'objet et son message ;elses'exécute si tout s'est bien passé ;finallys'exécute toujours.- Jamais de
except:nu ; garde le bloctryle plus court possible ; exceptions précises d'abord. raise ValueError("message")signale un problème et laisse l'appelant décider ;raiseseul 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()(commandesp,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
elseetfinallydans untry. - Je sais lever une exception avec
raiseet 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 pourquoiassertne doit pas valider une saisie. - Ma calculatrice résiste à toutes les saisies que j'ai pu imaginer, y compris Ctrl+C.