7 modèles de gestion d’erreurs Python que tout développeur devrait connaître

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 :

Python
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 :

Python
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) :

Python
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 :

Python
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 :

Python
# 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 :

Python
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 :

Python
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 :

Python
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 :

Python
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 :

Python
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 :

Python
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 :

Python
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.

Comments

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *