2.26. Lever des erreurs

Une fonction peut signaler un problème à son appelant en levant une exception. Le mot-clé est raise :

def square_root(x):
    if x < 0:
        raise ValueError("square_root expects a non-negative number")
    return x ** 0.5

Appeler square_root(-1) s’arrête à la ligne raise, sort de la fonction et recherche un except correspondant chez l’appelant. Si aucun appelant ne l’intercepte, le script se termine par une trace d’appels.

2.26.1. Pourquoi lever plutôt que renvoyer une valeur sentinelle

Deux façons de signaler une « mauvaise entrée » :

# signal with a sentinel
def square_root_or_none(x):
    if x < 0:
        return None
    return x ** 0.5

# raise an exception
def square_root(x):
    if x < 0:
        raise ValueError("...")
    return x ** 0.5

La forme par exception est généralement préférable :

  • L’appelant doit délibérément traiter le cas d’erreur – soit avec un try, soit en laissant l’exception se propager. Les sentinelles sont faciles à oublier et faciles à confondre avec un résultat normal.

  • Le message d’erreur voyage avec l’exception ; l’approche par sentinelle doit attacher le diagnostic ailleurs.

  • Le comportement par défaut face à une exception non gérée est un plantage bruyant accompagné d’une trace d’appels qui pointe vers l’appel fautif. Les retours silencieux de None se transforment plus tard en bugs subtils.

Ne recourez aux sentinelles que lorsque « introuvable » est un résultat courant et non exceptionnel – dict.get() renvoie None pour une clé absente précisément parce qu’il est attendu que les recherches échouent parfois.

2.26.2. Classes d’exception personnalisées

Pour lever un problème que l’appelant pourrait vouloir distinguer des erreurs intégrées, définissez une sous-classe de Exception :

class ConfigError(Exception):
    pass

def load_config(path):
    try:
        f = open(path)
    except OSError as e:
        raise ConfigError("missing config file: " + path)

try:
    load_config("settings.json")
except ConfigError as e:
    print("startup failed:", e)

Un corps de class vide convient – c’est le nom lui-même qui importe, car les appelants interceptent par classe. Regroupez les erreurs apparentées sous une base commune si un appelant pourrait vouloir intercepter toute la famille en un seul bloc.

2.26.2.1. Re-lever

Un raise nu à l’intérieur d’un bloc except relève l’exception courante afin qu’elle se propage au gestionnaire suivant :

try:
    do_work()
except Exception as e:
    log(e)
    raise        # let it keep going

C’est la bonne forme lorsqu’une fonction veut observer une erreur (la journaliser, la compter, annuler une modification partielle) sans réellement la traiter.

2.26.3. Quand intercepter et quand propager

Une règle empirique utile :

  • Interceptez une exception au niveau capable de récupérer utilement – substituer une valeur par défaut, réessayer, ignorer la mauvaise entrée.

  • Laissez-la se propager lorsqu’il n’y a rien d’utile à faire sinon planter, ou lorsque la couche supérieure est celle qui sait comment récupérer.

Une fonction au milieu d’une pile d’appels qui avale toutes les erreurs et renvoie silencieusement rend les défaillances introuvables. Préférez laisser les exceptions voyager jusqu’à ce qu’elles atteignent du code qui a véritablement un plan.