W3docs

Python raise et exceptions personnalisées

Apprenez à utiliser raise en Python, à chaîner les exceptions avec raise...from et à créer des classes d'exceptions personnalisées pour une gestion d'erreurs claire.

Python vous permet de faire bien plus que capturer des erreurs — vous pouvez aussi les signaler délibérément avec l'instruction raise et créer vos propres types d'exceptions pour représenter des problèmes spécifiques à votre domaine. Ce chapitre s'appuie sur Python Try...Except et couvre :

  • L'instruction raise — lever des exceptions intégrées
  • Relever des exceptions à l'intérieur d'un bloc except
  • Le chaînage d'exceptions avec raise ... from
  • La création de classes d'exceptions personnalisées
  • La construction d'une hiérarchie d'exceptions pour une application réelle
  • L'instruction assert et quand l'utiliser

L'instruction raise

L'instruction raise vous permet de lancer une exception à n'importe quel endroit de votre code. La forme la plus courante passe une instance d'exception avec un message descriptif :

raise ExceptionType("message")

Utilisez raise lorsque votre code détecte un problème que l'appelant doit gérer. Par exemple, une fonction qui accepte un âge doit rejeter immédiatement les valeurs négatives plutôt que de continuer silencieusement :

def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    return age

try:
    set_age(-1)
except ValueError as e:
    print(e)
# Output: Age cannot be negative

Choisir la bonne exception intégrée

Les types d'exceptions intégrés de Python sont porteurs de sens. Choisir le bon type rend votre API plus facile à comprendre et permet aux appelants de gérer différentes catégories d'erreurs séparément.

ExceptionQuand la lever
ValueErrorL'argument a le bon type mais une valeur invalide (age = -1)
TypeErrorL'argument a le mauvais type (age = "old")
KeyErrorUne clé de dictionnaire requise est manquante
IndexErrorUn index de séquence est hors limites
FileNotFoundErrorUn fichier requis n'existe pas
PermissionErrorLe processus ne dispose pas des droits pour effectuer une opération
RuntimeErrorUn problème d'exécution général qui ne correspond pas à un type plus précis
NotImplementedErrorUne méthode existe dans une classe de base mais doit être surchargée

Lever ValueError pour une mauvaise valeur est bien plus informatif que de lever une Exception brute, car les appelants peuvent écrire except ValueError pour gérer exactement ce cas.

Relever une exception

Parfois vous souhaitez effectuer une action lors d'une exception — la journaliser, nettoyer une ressource — puis laisser la même exception se propager vers l'appelant sans modification. Appelez raise sans argument à l'intérieur d'un bloc except pour relever l'exception courante :

def read_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError:
        print(f"Warning: config file not found at {path}")
        raise  # re-raise the original FileNotFoundError

try:
    read_config("missing.cfg")
except FileNotFoundError as e:
    print(f"Caught: {e}")
# Output:
# Warning: config file not found at missing.cfg
# Caught: [Errno 2] No such file or directory: 'missing.cfg'

Utiliser raise nu préserve la traceback originale, ce qui facilite grandement le débogage par rapport à capturer et relever e comme une nouvelle exception.

Chaînage d'exceptions avec raise ... from

Lorsque vous capturez une exception et en levez une autre, Python enregistre automatiquement l'exception originale comme contexte de la nouvelle. Vous pouvez rendre cette relation explicite — et significative — en utilisant raise NewException from original :

def load_data(path):
    try:
        with open(path) as f:
            return f.read()
    except OSError as e:
        raise RuntimeError("Failed to load configuration") from e

try:
    load_data("config.json")
except RuntimeError as e:
    print(f"Error: {e}")
    print(f"Caused by: {e.__cause__}")
# Output:
# Error: Failed to load configuration
# Caused by: [Errno 2] No such file or directory: 'config.json'

Lorsque Python affiche la traceback, il montre les deux exceptions dans l'ordre, indiquant clairement que la RuntimeError était une conséquence directe de l'OSError. C'est particulièrement utile dans le code de bibliothèque où vous souhaitez traduire des erreurs OS de bas niveau en erreurs de domaine de haut niveau sans masquer la cause racine.

Supprimer le chaînage avec raise ... from None

Parfois l'exception originale est un détail d'implémentation que vous ne souhaitez pas exposer. Passez None comme cause pour la masquer :

def fetch(url):
    try:
        raise ConnectionError("timeout")
    except ConnectionError:
        raise RuntimeError("Network unavailable") from None

try:
    fetch("http://example.com")
except RuntimeError as e:
    print(f"Error: {e}")
    print(f"Cause hidden: {e.__cause__}")
# Output:
# Error: Network unavailable
# Cause hidden: None

La traceback n'affichera que la RuntimeError. Utilisez cela avec parcimonie — masquer la cause racine complique le débogage pour les consommateurs de la bibliothèque.

Créer des classes d'exceptions personnalisées

Les exceptions intégrées couvrent les erreurs de programmation courantes, mais elles sont trop génériques pour les problèmes de domaine. Si votre application e-commerce lève un ValueError générique lors d'un échec de paiement, les appelants ne peuvent pas le distinguer d'un mauvais argument de fonction. Les classes d'exceptions personnalisées résolvent ce problème.

Une exception personnalisée est simplement une classe qui hérite d'Exception (ou d'une de ses sous-classes) :

class InsufficientFundsError(Exception):
    """Raised when a bank account has insufficient funds."""
    def __init__(self, amount, balance):
        self.amount = amount
        self.balance = balance
        super().__init__(
            f"Cannot withdraw {amount}: balance is only {balance}"
        )

class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(amount, self.balance)
        self.balance -= amount
        return self.balance

account = BankAccount(100)
try:
    account.withdraw(150)
except InsufficientFundsError as e:
    print(e)
    print(f"You tried to withdraw: {e.amount}")
    print(f"Available balance:     {e.balance}")
# Output:
# Cannot withdraw 150: balance is only 100
# You tried to withdraw: 150
# Available balance:     100

Points clés de ce modèle :

  • super().__init__(message) définit la chaîne lisible par l'humain renvoyée par str(e).
  • Les attributs supplémentaires (self.amount, self.balance) permettent aux appelants d'accéder à des données structurées depuis l'exception, pas seulement une chaîne.
  • Une docstring claire documente quand l'exception doit être levée.

Construire une hiérarchie d'exceptions

Les applications réelles ont souvent de nombreux types d'erreurs liés entre eux. Les regrouper sous une classe de base commune permet aux appelants de capturer soit l'erreur spécifique, soit toute la catégorie :

class AppError(Exception):
    """Base class for all application errors."""

class ValidationError(AppError):
    """Raised when user input fails validation."""

class DatabaseError(AppError):
    """Raised when a database operation fails."""

def validate_username(name):
    if len(name) < 3:
        raise ValidationError(f"Username '{name}' is too short (min 3 chars)")

try:
    validate_username("ab")
except ValidationError as e:
    print(f"Validation failed: {e}")
except AppError as e:
    print(f"Application error: {e}")
# Output:
# Validation failed: Username 'ab' is too short (min 3 chars)

Un appelant qui veut uniquement capturer les erreurs de base de données peut écrire except DatabaseError. Un appelant qui veut capturer tout problème de votre bibliothèque peut écrire except AppError. Cela reflète la conception de la propre hiérarchie d'exceptions de Python, où OSError regroupe FileNotFoundError, PermissionError et plusieurs autres.

Recommandations pour les exceptions personnalisées

  • Héritez d'Exception, pas de BaseException. BaseException est la racine de la hiérarchie Python et inclut également SystemExit et KeyboardInterrupt, qui ne doivent pas être capturés accidentellement.
  • Terminez le nom de la classe par Error pour les exceptions qui signalent un problème. Cela correspond à la nomenclature propre de Python (ValueError, TypeError, IOError).
  • Gardez la classe minimale sauf si vous avez besoin d'attributs supplémentaires. Un corps vide avec une docstring est parfaitement valide.
  • Placez les exceptions dans un module dédié (par exemple exceptions.py) pour les projets plus importants, afin que les appelants puissent les importer sans charger le reste de votre code.

L'instruction assert

assert est un moyen léger d'exprimer des invariants — des conditions qui doivent être vraies pour que votre code soit correct :

def divide(a, b):
    assert b != 0, "Divisor must not be zero"
    return a / b

try:
    divide(10, 0)
except AssertionError as e:
    print(f"AssertionError: {e}")

print(divide(10, 2))
# Output:
# AssertionError: Divisor must not be zero
# 5.0

assert condition, message lève AssertionError avec le message donné lorsque condition est False.

Limitation importante : Python supprime les instructions assert lorsqu'il est exécuté avec le drapeau -O (optimiser). Cela signifie :

  • Utilisez assert uniquement pour les vérifications de cohérence interne et les aides au débogage.
  • Utilisez raise avec une exception appropriée pour la validation des entrées utilisateur et les vérifications d'API publique qui doivent toujours s'exécuter.

Erreurs courantes

Capturer et ignorer silencieusement les exceptions

# Bad — the error disappears
try:
    result = risky_operation()
except Exception:
    pass

# Better — at minimum, log or re-raise
try:
    result = risky_operation()
except Exception as e:
    print(f"Operation failed: {e}")
    raise

Lever une chaîne au lieu d'une exception

# Wrong — strings are not exceptions
raise "something went wrong"  # TypeError

# Correct
raise ValueError("something went wrong")

Capturer BaseException accidentellement

# Dangerous — this catches KeyboardInterrupt and SystemExit too
except BaseException:
    ...

# Use Exception instead
except Exception:
    ...

Récapitulatif

TechniqueQuand l'utiliser
raise ExceptionType("msg")Signaler un problème que l'appelant doit gérer
raise (nu)Relever l'exception courante après journalisation ou nettoyage
raise NewError(...) from originalTraduire une erreur de bas niveau en erreur de haut niveau en préservant la cause
raise NewError(...) from NoneTraduire une erreur en masquant la cause interne
Classe d'exception personnaliséeDonner aux erreurs spécifiques au domaine un type unique et capturable
Hiérarchie d'exceptionsPermettre aux appelants de capturer des catégories d'erreurs étroites ou larges
assertVérifier les invariants internes pendant le développement uniquement

Pour une vue d'ensemble complète de la capture et de la gestion des exceptions, consultez Python Try...Except. Pour comprendre comment les exceptions personnalisées s'intègrent dans la conception des classes, consultez Python Classes and Objects et Python Inheritance.

Pratique

Pratique
Which statement correctly raises a ValueError with the message 'invalid input'?
Which statement correctly raises a ValueError with the message 'invalid input'?
Pratique
What does bare raise (with no argument) do inside an except block?
What does bare raise (with no argument) do inside an except block?
Pratique
Which base class should a custom exception inherit from?
Which base class should a custom exception inherit from?
Was this page helpful?