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.