Les Exceptions en Python
Qu'est-ce qu'une exception ?
Une exception est une erreur qui survient pendant l'exécution du programme (erreur dite "runtime"). Sans gestion des exceptions, le programme s'arrête brutalement et affiche un message d'erreur.
# Sans gestion d'exception
nombre = int("abc") # ValueError : l'exécution s'arrête ici
print("Cette ligne ne s'execute jamais")
ValueError: invalid literal for int() with base 10: 'abc'
Les exceptions permettent de détecter et de gérer ces situations d'erreur de façon contrôlée, sans planter le programme.
Les exceptions intégrées courantes
Python dispose de nombreuses exceptions prédéfinies :
| Exception | Quand elle se produit |
|---|---|
ValueError | Valeur incorrecte (ex: int("abc")) |
TypeError | Type incorrect (ex: "2" + 2) |
IndexError | Index hors limites (ex: liste[100]) |
KeyError | Clé absente d'un dict (ex: d["inexistant"]) |
FileNotFoundError | Fichier introuvable |
ZeroDivisionError | Division par zéro |
AttributeError | Attribut ou méthode inexistant |
ImportError | Import échoué |
NameError | Variable non définie |
StopIteration | Fin d'un itérateur |
# Exemples de chaque exception
liste = [1, 2, 3]
print(liste[10]) # IndexError
dico = {'a': 1}
print(dico['z']) # KeyError
print(10 / 0) # ZeroDivisionError
print("2" + 2) # TypeError
objet = None
objet.attribut # AttributeError
Bloc try/except : capturer les exceptions
La structure de base pour gérer les exceptions est le bloc try/except :
try:
# Code susceptible de provoquer une exception
resultat = 10 / 0
except ZeroDivisionError:
# Ce bloc s'exécute si ZeroDivisionError se produit
print("Division par zero impossible !")
# Exemple pratique : lecture d'un entier
def lire_entier(message):
try:
valeur = int(input(message))
return valeur
except ValueError:
print("Ce n'est pas un nombre entier valide.")
return None
age = lire_entier("Entrez votre age : ")
Capturer l'objet exception : except ExceptionType as e:
Pour accéder aux détails de l'erreur, utilisez as e :
try:
fichier = open("inexistant.txt", "r")
except FileNotFoundError as e:
print(f"Erreur : {e}")
print(f"Type : {type(e).__name__}")
# Affiche :
# Erreur : [Errno 2] No such file or directory: 'inexistant.txt'
# Type : FileNotFoundError
Plusieurs clauses except
On peut gérer différents types d'exceptions différemment :
def diviser(a, b):
try:
resultat = a / b
return resultat
except ZeroDivisionError:
print("Erreur : division par zero")
return None
except TypeError as e:
print(f"Erreur de type : {e}")
return None
print(diviser(10, 2)) # 5.0
print(diviser(10, 0)) # Erreur : division par zero => None
print(diviser(10, "a")) # Erreur de type : ... => None
Capturer plusieurs exceptions ensemble : except (Type1, Type2):
def convertir_et_diviser(valeur_str, diviseur_str):
try:
valeur = int(valeur_str)
diviseur = int(diviseur_str)
return valeur / diviseur
except (ValueError, ZeroDivisionError) as e:
print(f"Données invalides : {e}")
return None
print(convertir_et_diviser("10", "2")) # 5.0
print(convertir_et_diviser("abc", "2")) # Données invalides : ...
print(convertir_et_diviser("10", "0")) # Données invalides : ...
La clause else : si aucune exception
La clause else s'exécute uniquement si aucune exception n'a été levée dans le try :
def lire_fichier(nom_fichier):
try:
f = open(nom_fichier, 'r')
except FileNotFoundError:
print(f"Le fichier {nom_fichier} est introuvable.")
else:
# Pas d'exception : le fichier a été ouvert avec succès
contenu = f.read()
f.close()
print(f"Fichier lu : {len(contenu)} caracteres")
return contenu
La clause finally : exécution garantie
La clause finally s'exécute toujours, qu'il y ait eu une exception ou non. Elle sert typiquement au nettoyage (fermer un fichier, une connexion...).
def traiter_fichier(nom):
fichier = None
try:
fichier = open(nom, 'r')
contenu = fichier.read()
return contenu
except FileNotFoundError:
print("Fichier introuvable")
return None
except PermissionError:
print("Permission refusee")
return None
finally:
# Ce bloc s'exécute TOUJOURS, même si on a fait un return dans try/except
if fichier:
fichier.close()
print("Fichier ferme.")
La structure complète est donc : try → except → else → finally.
Lever une exception : raise
On peut lever (déclencher) délibérément une exception avec raise :
def calculer_racine(n):
if n < 0:
raise ValueError(f"Impossible de calculer la racine de {n} : nombre negatif")
import math
return math.sqrt(n)
try:
print(calculer_racine(16)) # 4.0
print(calculer_racine(-4)) # Leve ValueError
except ValueError as e:
print(f"Erreur : {e}")
def valider_age(age):
if not isinstance(age, int):
raise TypeError(f"L'age doit etre un entier, pas {type(age).__name__}")
if age < 0 or age > 150:
raise ValueError(f"L'age {age} n'est pas valide")
return age
Exceptions personnalisées
Vous pouvez créer vos propres exceptions en héritant de Exception :
class ErreurApplication(Exception):
"""Exception de base pour notre application."""
pass
class ErreurAuthentification(ErreurApplication):
"""Echec de l'authentification."""
def __init__(self, nom_utilisateur):
self.nom_utilisateur = nom_utilisateur
super().__init__(f"Authentification echouee pour : {nom_utilisateur}")
class ErreurSoldeInsuffisant(ErreurApplication):
"""Solde insuffisant pour effectuer l'operation."""
def __init__(self, solde, montant):
self.solde = solde
self.montant = montant
super().__init__(
f"Solde insuffisant : {solde} disponible, {montant} requis"
)
def retirer(solde, montant):
if montant > solde:
raise ErreurSoldeInsuffisant(solde, montant)
return solde - montant
try:
nouveau_solde = retirer(100, 150)
except ErreurSoldeInsuffisant as e:
print(f"Erreur : {e}")
print(f"Il vous manque {e.montant - e.solde} euros")
Chaining d'exceptions : raise ... from
Quand on re-lève une exception depuis un handler, on peut conserver le contexte original :
class ErreurBase(Exception):
pass
def charger_config(chemin):
try:
with open(chemin) as f:
import json
return json.load(f)
except FileNotFoundError as e:
raise ErreurBase(f"Configuration introuvable : {chemin}") from e
except json.JSONDecodeError as e:
raise ErreurBase(f"Configuration JSON invalide : {chemin}") from e
try:
config = charger_config("config.json")
except ErreurBase as e:
print(f"Erreur : {e}")
print(f"Cause originale : {e.__cause__}")
raise NewError(...) from original_error lie les deux exceptions. raise NewError(...) seul utilise __context__ implicitement.
Context managers et exceptions
Les context managers (with statement) gèrent automatiquement les ressources et appellent le nettoyage même en cas d'exception :
# Sans context manager : risque de ne pas fermer le fichier
try:
f = open("fichier.txt", "r")
contenu = f.read()
f.close()
except FileNotFoundError:
print("Fichier introuvable")
# Avec context manager : fermeture garantie
try:
with open("fichier.txt", "r") as f:
contenu = f.read()
# f est fermé automatiquement ici, même en cas d'exception
except FileNotFoundError:
print("Fichier introuvable")
Bonnes pratiques
Soyez spécifique dans les exceptions capturées
# Mauvais : trop large, masque les bugs
try:
faire_quelque_chose()
except Exception:
pass # Les bugs disparaissent silencieusement !
# Bon : specifique et informatif
try:
valeur = int(saisie_utilisateur)
except ValueError:
print("Veuillez entrer un nombre entier")
Ne silenciez pas les erreurs
# Mauvais
try:
resultat = risque()
except Exception:
pass # On ignore l'erreur... tres dangereux
# Acceptable (avec log)
import logging
try:
resultat = risque()
except ValueError as e:
logging.warning(f"Valeur inattendue : {e}")
resultat = valeur_par_defaut
Levez des exceptions au bon niveau
# La validation leve une exception
def valider_email(email):
if '@' not in email:
raise ValueError(f"Email invalide : {email}")
return email
# L'appelant gere l'erreur au bon endroit
def inscrire_utilisateur(email, nom):
try:
email_valide = valider_email(email)
except ValueError as e:
print(f"Inscription impossible : {e}")
return False
# ... continuer l'inscription
return True
Exercices pratiques
Exercice 1 : try/except pour une division par zéro
Mettez le moins de code possible dans le bloc try. Un bloc try énorme rend difficile l'identification de quelle ligne a provoqué l'exception. Isolez précisément les instructions susceptibles d'échouer.
Exercice 2 : try/except/else/finally
Utilisez else pour le code qui ne doit s'exécuter que si le try a réussi (cela clarifie l'intention). Utilisez finally pour le nettoyage de ressources. En pratique, le context manager with remplace souvent finally pour la gestion de fichiers et connexions.
Exercice 3 : Lever une ValueError avec message personnalisé
Incluez toujours une description claire dans le message de votre exception, et si possible la valeur problématique. raise ValueError(f"Age invalide : {age}") est bien plus utile que raise ValueError("Valeur invalide"). Le développeur qui débogue votre code vous remerciera.
Exercice 4 : Créer une exception personnalisée
Créez une exception de base pour votre application (ex: ErreurApp) et faites hériter toutes vos exceptions spécifiques. Cela permet aux utilisateurs de capturer soit une exception précise, soit toutes les exceptions de votre app avec except ErreurApp. C'est le même principe que IOError et FileNotFoundError dans la bibliothèque standard.
Exercice 5 : Gérer plusieurs types d'exceptions
Regroupez les exceptions avec except (Type1, Type2) quand le traitement est identique pour les deux. Si le traitement diffère, utilisez deux clauses except séparées. Ne regroupez jamais des exceptions très différentes juste pour réduire le nombre de lignes.