Gestion des erreurs et exceptions#
La hiérarchie des exceptions#
Python dispose d’un système d’exceptions structuré en une hiérarchie de classes. Comprendre cette hiérarchie est indispensable pour écrire des gestionnaires d’erreurs précis et pour créer ses propres exceptions de façon cohérente.
Au sommet se trouve BaseException, la classe mère de toutes les exceptions. Elle a quatre sous-classes directes :
SystemExit: levée parsys.exit(). Hérite deBaseExceptionet non deExceptionafin que les blocsexcept Exceptionne l’attrapent pas accidentellement.KeyboardInterrupt: levée quand l’utilisateur appuie sur Ctrl+C.GeneratorExit: levée quand un générateur ou une coroutine est fermé via.close().Exception: la classe mère de toutes les exceptions applicatives. C’est de celle-ci que dérivent toutes les exceptions que vous utiliserez au quotidien.
La quasi-totalité des exceptions que l’on rencontre en pratique héritent de Exception. En voici les plus importantes :
Classe |
Situation typique |
|---|---|
|
Argument du bon type mais valeur invalide ( |
|
Opération sur le mauvais type ( |
|
Clé absente dans un dictionnaire |
|
Indice hors bornes dans une séquence |
|
Accès à un attribut inexistant |
|
Utilisation d’une variable non définie |
|
Erreurs liées au système d’exploitation (fichiers, réseau) |
|
Fichier introuvable (sous-classe d” |
|
Droits insuffisants (sous-classe d” |
|
Signal de fin d’un itérateur |
|
Erreur générique à l’exécution |
|
Méthode abstraite non implémentée |
|
Classe mère des erreurs arithmétiques |
|
Division par zéro (sous-classe d” |
|
Dépassement de capacité numérique |
|
Mémoire insuffisante |
|
Profondeur de récursion maximale dépassée |
|
Échec d’importation d’un module |
|
Module introuvable (sous-classe d” |
|
Assertion |
try / except / else / finally#
La syntaxe complète du bloc de gestion d’exceptions en Python comprend quatre clauses, chacune avec un rôle distinct :
try:
# Code susceptible de lever une exception
...
except SomeException as e:
# Exécuté si SomeException (ou une sous-classe) est levée
...
except (TypeError, ValueError) as e:
# Plusieurs types dans un seul except
...
else:
# Exécuté UNIQUEMENT si aucune exception n'a été levée dans try
...
finally:
# Exécuté TOUJOURS, exception ou non
...
La clause else est souvent oubliée, mais elle joue un rôle sémantique important : elle sépare le code qui peut échouer du code qui suit le succès. Cela évite d’attraper accidentellement des exceptions levées par le code de traitement post-succès :
def lire_entier(chaine: str) -> int | None:
try:
valeur = int(chaine)
except ValueError:
print(f"'{chaine}' n'est pas un entier valide.")
return None
else:
# Exécuté seulement si int(chaine) a réussi
print(f"Conversion réussie : {valeur}")
return valeur
finally:
# Toujours exécuté, utile pour le nettoyage
print("Fin de lire_entier().")
print(lire_entier("42"))
print()
print(lire_entier("abc"))
Conversion réussie : 42
Fin de lire_entier().
42
'abc' n'est pas un entier valide.
Fin de lire_entier().
None
Remarque 26
La clause finally est garantie de s’exécuter dans tous les cas : après une sortie normale, après un except, après un return, après un break ou un continue dans une boucle, et même après une exception non attrapée. C’est pourquoi elle est idéale pour les opérations de nettoyage (fermeture de fichiers, libération de verrous), bien que les gestionnaires de contexte soient généralement préférables pour cela.
def demonstrer_finally():
try:
print("Dans try.")
return "valeur du try" # Le finally s'exécute quand même !
finally:
print("Dans finally (même après return).")
resultat = demonstrer_finally()
print(f"Résultat : {resultat}")
Dans try.
Dans finally (même après return).
Résultat : valeur du try
On peut empiler plusieurs clauses except pour traiter différentes exceptions de façon spécifique. L’ordre est important : Python teste les clauses de haut en bas et s’arrête à la première correspondance. Il faut donc toujours placer les classes les plus spécifiques avant les classes plus générales :
import json
def charger_config(chemin: str) -> dict:
try:
with open(chemin, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
print(f"Fichier introuvable : {chemin}")
return {}
except PermissionError:
print(f"Droits insuffisants pour lire : {chemin}")
return {}
except json.JSONDecodeError as e:
print(f"JSON invalide dans {chemin} : {e}")
return {}
except OSError as e:
# Plus général : attrape FileNotFoundError et PermissionError
# si elles n'avaient pas été listées avant
print(f"Erreur système : {e}")
return {}
resultat = charger_config("/tmp/config_inexistante.json")
print(f"Résultat : {resultat}")
Fichier introuvable : /tmp/config_inexistante.json
Résultat : {}
Lever une exception#
raise#
L’instruction raise lève une exception. On peut lui passer une instance d’exception ou une classe (Python instancie alors la classe sans arguments) :
def diviser(a: float, b: float) -> float:
if b == 0:
raise ValueError("Le diviseur ne peut pas être zéro.")
return a / b
try:
resultat = diviser(10, 0)
except ValueError as e:
print(f"Erreur : {e}")
Erreur : Le diviseur ne peut pas être zéro.
raise from — chaîner les causes#
La syntaxe raise NouvelleException from cause attache l’exception originale comme cause explicite de la nouvelle. C’est une pratique essentielle pour les bibliothèques qui transforment des exceptions bas niveau en abstractions de plus haut niveau, sans perdre le contexte d’origine :
class ErreurConnexion(Exception):
"""Exception de haut niveau pour les erreurs de connexion."""
def connecter_base(url: str) -> None:
try:
# Simulons une erreur bas niveau
raise ConnectionRefusedError(f"Port fermé sur {url}")
except ConnectionRefusedError as e:
raise ErreurConnexion(
f"Impossible de se connecter à la base de données : {url}"
) from e
try:
connecter_base("postgresql://localhost:5432/ma_base")
except ErreurConnexion as e:
print(f"Erreur : {e}")
print(f"Cause originale : {e.__cause__}")
Erreur : Impossible de se connecter à la base de données : postgresql://localhost:5432/ma_base
Cause originale : Port fermé sur postgresql://localhost:5432/ma_base
raise nu — re-lever une exception#
À l’intérieur d’un bloc except, un raise sans argument re-lève l’exception courante sans la modifier. C’est utile pour journaliser une erreur et la propager quand même :
import logging
def operation_critique():
try:
raise RuntimeError("Quelque chose s'est mal passé.")
except RuntimeError:
# On journalise sans avaler l'exception
print("[LOG] Erreur capturée, propagation en cours...")
raise # Re-lève RuntimeError telle quelle
try:
operation_critique()
except RuntimeError as e:
print(f"Exception propagée reçue : {e}")
[LOG] Erreur capturée, propagation en cours...
Exception propagée reçue : Quelque chose s'est mal passé.
Définir ses propres exceptions#
La convention Python est de créer une hiérarchie d’exceptions spécifiques à son domaine en héritant de Exception (ou d’une sous-classe appropriée). Les exceptions personnalisées peuvent avoir des attributs supplémentaires pour transporter des informations contextuelles.
class ErreurApplication(Exception):
"""Classe de base pour toutes les exceptions de l'application."""
class ErreurValidation(ErreurApplication):
"""Erreur de validation des données entrantes."""
def __init__(self, champ: str, valeur, message: str):
self.champ = champ
self.valeur = valeur
super().__init__(f"[{champ}={valeur!r}] {message}")
class ErreurAuthentification(ErreurApplication):
"""Erreur d'authentification."""
def __init__(self, utilisateur: str, raison: str = ""):
self.utilisateur = utilisateur
self.raison = raison
super().__init__(f"Authentification échouée pour '{utilisateur}'. {raison}")
# Utilisation
def valider_age(age: int) -> None:
if not isinstance(age, int):
raise ErreurValidation("age", age, "Doit être un entier.")
if age < 0 or age > 150:
raise ErreurValidation("age", age, "Doit être entre 0 et 150.")
try:
valider_age(-5)
except ErreurValidation as e:
print(f"Validation échouée — champ : {e.champ}, valeur : {e.valeur}")
print(f"Message : {e}")
Validation échouée — champ : age, valeur : -5
Message : [age=-5] Doit être entre 0 et 150.
Définition 27 (Hiérarchie d’exceptions métier)
Une hiérarchie d’exceptions métier est un ensemble de classes d’exceptions organisées de façon à refléter le domaine de l’application. La classe racine (par exemple ErreurApplication) permet d’attraper toutes les erreurs de l’application avec un seul except, tandis que les sous-classes permettent un traitement précis de chaque type d’erreur. Cette organisation facilite la maintenance et la lisibilité du code de gestion d’erreurs.
Bonnes pratiques#
Attraper le plus spécifique possible#
Il faut éviter les clauses except Exception ou pire, les except: nus (qui attrapent même KeyboardInterrupt et SystemExit). Attraper une exception trop générale masque des erreurs réelles :
# Mauvaise pratique : attrape tout, même KeyboardInterrupt
try:
faire_quelque_chose()
except:
pass # "Avaler" silencieusement une exception est presque toujours une erreur.
# Bonne pratique
try:
valeur = int(entree_utilisateur)
except ValueError:
valeur = 0 # On sait exactement ce que l'on gère.
Ne jamais avaler silencieusement#
Un except SomeException: pass efface silencieusement des erreurs qui pourraient indiquer des bugs sérieux. Si l’on doit ignorer une exception, contextlib.suppress ou au moins un commentaire explicatif est préférable :
import contextlib
# Acceptable : l'intention est claire et documentée
with contextlib.suppress(FileNotFoundError):
import os
os.remove("/tmp/fichier_temporaire.txt")
Journalisation avec logging#
La bibliothèque standard logging est bien plus adaptée que print pour enregistrer les erreurs. logging.exception() inclut automatiquement le traceback complet :
import logging
logging.basicConfig(level=logging.DEBUG,
format='%(levelname)s: %(message)s')
def operation_avec_log():
try:
resultat = 1 / 0
except ZeroDivisionError:
logging.exception("Division par zéro lors du calcul.")
return None
operation_avec_log()
ERROR: Division par zéro lors du calcul.
Traceback (most recent call last):
File "/tmp/ipykernel_9382/263915616.py", line 8, in operation_avec_log
resultat = 1 / 0
~~^~~
ZeroDivisionError: division by zero
Le module warnings#
Pour les situations qui ne sont pas des erreurs fatales mais méritent l’attention (fonctionnalités dépréciées, comportements ambigus), Python fournit warnings.warn() :
import warnings
def ancienne_fonction(x: int) -> int:
warnings.warn(
"ancienne_fonction() est dépréciée, utilisez nouvelle_fonction() à la place.",
DeprecationWarning,
stacklevel=2 # Pointe vers l'appelant, pas vers cette ligne
)
return x * 2
resultat = ancienne_fonction(5)
print(f"Résultat : {resultat}")
Résultat : 10
/tmp/ipykernel_9382/1552801776.py:11: DeprecationWarning: ancienne_fonction() est dépréciée, utilisez nouvelle_fonction() à la place.
resultat = ancienne_fonction(5)
ExceptionGroup (Python 3.11+)#
Python 3.11 a introduit ExceptionGroup et la syntaxe except* pour gérer plusieurs exceptions survenues simultanément, un besoin qui émerge naturellement dans les environnements concurrents (tâches asyncio, exécuteurs parallèles) où plusieurs sous-tâches peuvent échouer en même temps.
# Python 3.11+
import asyncio
async def tache(n: int) -> None:
if n % 2 == 0:
raise ValueError(f"Valeur paire interdite : {n}")
await asyncio.sleep(0.01)
async def main():
async with asyncio.TaskGroup() as tg:
for i in range(5):
tg.create_task(tache(i))
# TaskGroup lève automatiquement un ExceptionGroup
# si plusieurs tâches échouent.
La syntaxe except* filtre les exceptions d’un groupe par type, permettant de traiter chaque type séparément tout en laissant les autres se propager :
# Création manuelle d'un ExceptionGroup pour la démonstration
groupe = ExceptionGroup(
"Erreurs de validation",
[
ValueError("Âge invalide"),
TypeError("Type incorrect"),
ValueError("Nom vide"),
]
)
try:
raise groupe
except* ValueError as eg:
print(f"ValueError capturées ({len(eg.exceptions)}) :")
for e in eg.exceptions:
print(f" - {e}")
except* TypeError as eg:
print(f"TypeError capturées : {eg.exceptions}")
ValueError capturées (2) :
- Âge invalide
- Nom vide
TypeError capturées : (TypeError('Type incorrect'),)
Remarque 27
ExceptionGroup ne remplace pas la gestion d’exceptions classique : pour les erreurs séquentielles ordinaires, try / except reste la bonne approche. ExceptionGroup est conçu spécifiquement pour les scénarios de concurrence où plusieurs opérations indépendantes s’exécutent en parallèle et peuvent échouer simultanément. La bibliothèque anyio et asyncio.TaskGroup en sont les principaux producteurs.
Résumé#
Ce chapitre a couvert la gestion des erreurs et des exceptions en Python de façon complète :
La hiérarchie des exceptions part de
BaseException, avecExceptioncomme racine des exceptions applicatives.SystemExit,KeyboardInterruptetGeneratorExithéritent directement deBaseExceptionpour ne pas être attrapées accidentellement.La syntaxe
try / except / else / finallyest la structure principale.elses’exécute uniquement en cas de succès, séparant clairement le code susceptible d’échouer du code de traitement.finallys’exécute toujours.raiselève une exception,raise X from Ychaîne les causes, etraisenu re-lève l’exception courante.Les exceptions personnalisées s’écrivent en héritant d”
Exception, avec une hiérarchie reflétant le domaine métier et des attributs portant le contexte de l’erreur.Les bonnes pratiques consistent à attraper le plus spécifique possible, ne jamais avaler silencieusement, journaliser avec
logging.exception(), et utiliserwarnings.warn()pour les dépréciations.ExceptionGroupetexcept*(Python 3.11+) permettent de gérer des exceptions multiples survenues simultanément dans un contexte concurrent.
Dans le chapitre suivant, nous aborderons les annotations de types et l’outil mypy, qui permettent de détecter statiquement toute une classe d’erreurs avant même d’exécuter le code.