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
assertet 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 negativeChoisir 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.
| Exception | Quand la lever |
|---|---|
ValueError | L'argument a le bon type mais une valeur invalide (age = -1) |
TypeError | L'argument a le mauvais type (age = "old") |
KeyError | Une clé de dictionnaire requise est manquante |
IndexError | Un index de séquence est hors limites |
FileNotFoundError | Un fichier requis n'existe pas |
PermissionError | Le processus ne dispose pas des droits pour effectuer une opération |
RuntimeError | Un problème d'exécution général qui ne correspond pas à un type plus précis |
NotImplementedError | Une 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: NoneLa 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: 100Points clés de ce modèle :
super().__init__(message)définit la chaîne lisible par l'humain renvoyée parstr(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 deBaseException.BaseExceptionest la racine de la hiérarchie Python et inclut égalementSystemExitetKeyboardInterrupt, qui ne doivent pas être capturés accidentellement. - Terminez le nom de la classe par
Errorpour 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.0assert 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
assertuniquement pour les vérifications de cohérence interne et les aides au débogage. - Utilisez
raiseavec 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}")
raiseLever 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
| Technique | Quand 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 original | Traduire une erreur de bas niveau en erreur de haut niveau en préservant la cause |
raise NewError(...) from None | Traduire une erreur en masquant la cause interne |
| Classe d'exception personnalisée | Donner aux erreurs spécifiques au domaine un type unique et capturable |
| Hiérarchie d'exceptions | Permettre aux appelants de capturer des catégories d'erreurs étroites ou larges |
assert | Vé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.