La gestion des erreurs est l’une des compétences les plus importantes pour écrire du code Python robuste et maintenable. Un programme qui ignore les erreurs finira tôt ou tard par produire des résultats incorrects ou par planter en production. Dans cet article, nous explorons 7 modèles que tout développeur Python devrait maîtriser.
1. try/except : la base de la gestion d’erreurs
Le bloc try/except est le mécanisme fondamental de gestion d’erreurs en Python. Il permet de capturer une exception et de réagir de manière appropriée :
def diviser(a, b):
try:
resultat = a / b
except ZeroDivisionError:
print("Erreur : division par zéro impossible")
return None
except TypeError:
print("Erreur : les arguments doivent être des nombres")
return None
return resultat
print(diviser(10, 3)) # 3.333...
print(diviser(10, 0)) # None (avec message d'erreur)
print(diviser("a", 2)) # None (avec message d'erreur)
Bonne pratique : capturez toujours des exceptions spécifiques plutôt qu’un except Exception générique. Cela rend le code plus lisible et évite de masquer des erreurs inattendues.
Vous pouvez aussi accéder à l’objet exception avec le mot-clé as :
try:
valeur = int("abc")
except ValueError as e:
print(f"Conversion impossible : {e}")
# Conversion impossible : invalid literal for int() with base 10: 'abc'
2. finally : nettoyage garanti
Le bloc finally s’exécute toujours, qu’une exception ait été levée ou non. Il est indispensable pour libérer des ressources (fichiers, connexions, verrous) :
def lire_fichier(chemin):
fichier = None
try:
fichier = open(chemin, "r")
contenu = fichier.read()
return contenu
except FileNotFoundError:
print(f"Fichier introuvable : {chemin}")
return None
except PermissionError:
print(f"Permission refusée : {chemin}")
return None
finally:
if fichier is not None:
fichier.close()
print("Fichier fermé correctement")
Le bloc finally s’exécute même si le bloc try contient un return. C’est ce qui le rend si fiable pour le nettoyage de ressources.
Astuce : combinez else avec try/except/finally pour séparer clairement le code normal du code de gestion d’erreurs :
try:
resultat = calculer(donnees)
except ValueError as e:
print(f"Erreur de calcul : {e}")
else:
print(f"Résultat : {resultat}") # seulement si pas d'exception
finally:
print("Calcul terminé") # toujours exécuté
3. Le gestionnaire de contexte (with)
L’instruction with est la manière la plus pythonique de gérer des ressources. Elle garantit que le nettoyage est effectué automatiquement, même en cas d’exception :
# Le fichier est automatiquement fermé à la sortie du bloc
with open("donnees.txt", "r") as f:
contenu = f.read()
# Fonctionne avec de nombreuses ressources
import sqlite3
with sqlite3.connect("base.db") as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM utilisateurs")
resultats = cursor.fetchall()
Vous pouvez créer vos propres gestionnaires de contexte avec contextlib.contextmanager :
from contextlib import contextmanager
import time
@contextmanager
def chronometre(label):
debut = time.perf_counter()
try:
yield
finally:
duree = time.perf_counter() - debut
print(f"{label} : {duree:.4f} secondes")
with chronometre("Calcul"):
total = sum(range(1_000_000))
print(f"Total : {total}")
Les context managers remplacent avantageusement le pattern try/finally pour la gestion de ressources. Préférez-les chaque fois que c’est possible.
4. Lever des exceptions avec raise
L’instruction raise permet de signaler explicitement qu’une situation anormale s’est produite. C’est essentiel pour valider les entrées et les préconditions :
def valider_age(age):
if not isinstance(age, int):
raise TypeError(
f"L'age doit être un entier, reçu : {type(age).__name__}"
)
if age < 0:
raise ValueError(f"L'age ne peut pas être négatif : {age}")
if age > 150:
raise ValueError(f"L'age semble invalide : {age}")
return True
try:
valider_age(-5)
except ValueError as e:
print(f"Validation échouée : {e}")
Utilisez raise pour échouer rapidement (fail fast) dès qu’une condition invalide est détectée. Cela évite de propager des données incorrectes à travers le programme.
Pour relancer une exception capturée (en conservant la trace d’appels originale), utilisez simplement raise sans argument :
try:
traiter_donnees()
except ValueError:
print("Journalisation de l'erreur...")
raise # relance l'exception originale
5. Exceptions personnalisées
Créer vos propres exceptions rend votre code plus expressif et facilite la gestion d’erreurs pour les utilisateurs de votre bibliothèque :
class AppError(Exception):
"""Classe de base pour les erreurs de l'application."""
pass
class ValidationError(AppError):
"""Erreur de validation des données."""
def __init__(self, champ, message):
self.champ = champ
self.message = message
super().__init__(f"{champ} : {message}")
class AuthenticationError(AppError):
"""Erreur d'authentification."""
pass
def creer_utilisateur(nom, email):
if not nom or len(nom) < 2:
raise ValidationError("nom", "doit contenir au moins 2 caractères")
if "@" not in email:
raise ValidationError("email", "format invalide")
return {"nom": nom, "email": email}
try:
utilisateur = creer_utilisateur("", "invalide")
except ValidationError as e:
print(f"Erreur de validation - {e.champ} : {e.message}")
except AppError as e:
print(f"Erreur application : {e}")
Bonne pratique : créez une hiérarchie d’exceptions avec une classe de base commune (AppError). Cela permet aux appelants de capturer toutes les erreurs de votre application avec un seul except AppError ou de cibler des erreurs spécifiques.
Ajoutez des attributs personnalisés (comme champ et message) pour fournir un contexte riche aux gestionnaires d’erreurs en amont.
6. Assertions pour le débogage et les invariants
L’instruction assert vérifie qu’une condition est vraie et lève une AssertionError dans le cas contraire. Elle est idéale pour documenter et vérifier les hypothèses de votre code :
def calculer_moyenne(notes):
assert isinstance(notes, list), "Les notes doivent être une liste"
assert len(notes) > 0, "La liste de notes ne peut pas être vide"
assert all(0 <= n <= 20 for n in notes), "Chaque note doit être entre 0 et 20"
return sum(notes) / len(notes)
print(calculer_moyenne([15, 12, 18])) # 15.0
try:
calculer_moyenne([])
except AssertionError as e:
print(f"Assertion échouée : {e}")
Important : les assertions peuvent être désactivées globalement avec l’option python -O (mode optimisé). Par conséquent, ne les utilisez jamais pour valider des entrées utilisateur ou pour implémenter une logique métier critique.
Réservez assert pour les vérifications internes : invariants de boucle, préconditions de fonctions privées et détection de bugs pendant le développement.
7. Logique de retry (nouvelle tentative)
Les appels réseau, les accès aux bases de données et les opérations sur des fichiers distants peuvent échouer de manière transitoire. Une logique de retry avec backoff exponentiel permet de gérer ces défaillances temporaires :
import time
import random
def avec_retry(func, max_tentatives=3, delai_base=1.0):
"""Exécute une fonction avec logique de retry et backoff exponentiel."""
for tentative in range(1, max_tentatives + 1):
try:
return func()
except Exception as e:
if tentative == max_tentatives:
print(f"Echec après {max_tentatives} tentatives")
raise
delai = delai_base * (2 ** (tentative - 1))
delai += random.uniform(0, delai * 0.1)
print(f"Tentative {tentative}/{max_tentatives} échouée : {e}")
print(f"Nouvelle tentative dans {delai:.1f}s...")
time.sleep(delai)
Pour une solution plus élégante et réutilisable, transformez cette logique en décorateur :
import time
import functools
def retry(max_tentatives=3, delai=1.0, exceptions=(Exception,)):
"""Décorateur de retry avec backoff exponentiel."""
def decorateur(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for tentative in range(1, max_tentatives + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if tentative == max_tentatives:
raise
attente = delai * (2 ** (tentative - 1))
time.sleep(attente)
return wrapper
return decorateur
@retry(max_tentatives=3, delai=0.5, exceptions=(ConnectionError, TimeoutError))
def appeler_api():
print("Appel de l'API...")
raise ConnectionError("Serveur indisponible")
try:
appeler_api()
except ConnectionError:
print("L'API est définitivement indisponible")
Le backoff exponentiel (1s, 2s, 4s, …) évite de surcharger un service déjà en difficulté. L’ajout d’un jitter (variation aléatoire) empêche que de multiples clients ne retentent tous au même instant.
Conclusion
Ces 7 modèles forment un kit complet pour gérer les erreurs en Python de manière professionnelle :
– try/except pour capturer et traiter les exceptions
– finally pour garantir le nettoyage des ressources
– with pour une gestion de ressources élégante et sûre
– raise pour signaler les conditions anormales
– Exceptions personnalisées pour un code expressif et une hiérarchie d’erreurs claire
– assert pour documenter et vérifier les invariants
– Retry pour gérer les défaillances transitoires avec élégance
Un bon code ne se contente pas de fonctionner quand tout va bien : il gère aussi les situations imprévues avec grâce. En appliquant ces modèles, vous rendrez votre code plus robuste, plus lisible et plus facile à déboguer.

Laisser un commentaire