Aller au contenu principal

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 :

ExceptionQuand elle se produit
ValueErrorValeur incorrecte (ex: int("abc"))
TypeErrorType incorrect (ex: "2" + 2)
IndexErrorIndex hors limites (ex: liste[100])
KeyErrorClé absente d'un dict (ex: d["inexistant"])
FileNotFoundErrorFichier introuvable
ZeroDivisionErrorDivision par zéro
AttributeErrorAttribut ou méthode inexistant
ImportErrorImport échoué
NameErrorVariable non définie
StopIterationFin 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 : tryexceptelsefinally.


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

Bonne pratique - Bloc try minimal

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

Bonne pratique - Utiliser else et 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é

Bonne pratique - Messages d'erreur explicites

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

Bonne pratique - Hiérarchie d'exceptions"

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

Bonne pratique - Grouper les exceptions similaires

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.


Quiz de révision


Quelle clause s'exécute TOUJOURS, qu'il y ait eu une exception ou non ?


Que fait 'raise ValueError("message")' ?


Quelle exception est levée quand on accède à un index inexistant dans une liste ?


Quand la clause 'else' d'un try/except s'exécute-t-elle ?


Comment créer une exception personnalisée en Python ?


📌 Une solution