Python : Les exceptions
try / except / else / finally, with, raise, exceptions personnalisées et le style EAFP : gérer les erreurs à la Python.
Python : Les exceptions
Comme en C#, une erreur à l'exécution lève une exception qui remonte la pile d'appels jusqu'à être rattrapée, ou fait planter le programme. La mécanique est très proche de C# : Les exceptions. La vraie différence est culturelle : en Python, utiliser les exceptions pour le flux normal du programme n'est pas un anti-pattern, c'est le style recommandé.
# Qu'est-ce qu'une exception ?
Une exception est un objet, instance d'une classe qui hérite de Exception. Non rattrapée, elle arrête le programme et affiche une traceback : la pile des appels, du plus ancien au plus récent, avec l'erreur tout en bas.
nombres = [1, 2, 3]
nombres[10]nombres = [1, 2, 3]nombres[10]Traceback (most recent call last):
File "main.py", line 2, in <module>
nombres[10]
IndexError: list index out of rangeTraceback (most recent call last): File "main.py", line 2, in <module> nombres[10]IndexError: list index out of rangeSe lit de bas en haut : le type et le message d'abord, puis la ligne fautive juste au-dessus.
# try / except
try entoure le code risqué, except rattrape un type d'exception précis. as e donne accès à l'objet exception.
try:
age = int(saisie)
except ValueError as e:
print(f"Saisie invalide : {e}")try: age = int(saisie)except ValueError as e: print(f"Saisie invalide : {e}")Plusieurs types se gèrent avec plusieurs except, ou un tuple quand le traitement est le même :
try:
valeur = config["port"] / diviseur
except KeyError:
print("Clé 'port' absente")
except (ZeroDivisionError, TypeError):
print("Calcul impossible")try: valeur = config["port"] / diviseurexcept KeyError: print("Clé 'port' absente")except (ZeroDivisionError, TypeError): print("Calcul impossible")L'ordre compte : le premier except qui correspond gagne. Du plus spécifique au plus général, comme en C#. Mais là où le compilateur C# refuse un ordre incohérent, Python l'accepte sans broncher : le except spécifique placé après un général ne sera tout simplement jamais atteint.
N'écris jamais un except: nu. Il rattrape tout, y compris KeyboardInterrupt (Ctrl+C) et SystemExit : ton script devient impossible à interrompre. Au pire, except Exception:, et seulement pour logger puis relancer.
# else et finally
Deux blocs optionnels complètent la structure :
try:
age = int(saisie)
except ValueError:
print("Saisie invalide")
else:
enregistrer(age) # seulement si la conversion a réussi
finally:
print("Saisie traitée") # dans tous les castry: age = int(saisie)except ValueError: print("Saisie invalide")else: enregistrer(age) # seulement si la conversion a réussifinally: print("Saisie traitée") # dans tous les caselses'exécute seulement si letryn'a levé aucune exception. Il n'existe pas en C#. Son intérêt : garder letryminimal. Ici, sienregistrer()lève à son tour uneValueError, elle ne sera pas prise à tort pour une « saisie invalide ».finallys'exécute toujours, exception ou pas, même après unreturn. C'est la place du nettoyage.
# with : le finally automatique
Fermer une ressource dans un finally est si courant que Python a une syntaxe dédiée : with, l'équivalent du using de C#.
with open("donnees.csv") as fichier:
lignes = fichier.readlines()
# ici, le fichier est fermé, même si readlines() a levé une exceptionwith open("donnees.csv") as fichier: lignes = fichier.readlines()# ici, le fichier est fermé, même si readlines() a levé une exceptionÇa fonctionne avec tout objet qui est un context manager : fichiers, verrous, connexions à une base, transactions. Dès qu'une ressource doit être libérée, le réflexe est with, pas try/finally.
# Lever une exception : raise
raise lève une exception, l'équivalent du throw de C#.
def retirer(self, montant):
if montant <= 0:
raise ValueError("Le montant doit être positif")
if montant > self.solde:
raise SoldeInsuffisantError(montant, self.solde)
self.solde -= montantdef retirer(self, montant): if montant <= 0: raise ValueError("Le montant doit être positif") if montant > self.solde: raise SoldeInsuffisantError(montant, self.solde) self.solde -= montantDans un except, un raise seul relance l'exception en cours, avec sa traceback intacte. Pratique pour logger sans avaler l'erreur :
try:
reponse = appeler_service()
except ConnectionError:
logger.error("Service indisponible")
raise # relance telle quelletry: reponse = appeler_service()except ConnectionError: logger.error("Service indisponible") raise # relance telle quellePour traduire une erreur technique en erreur métier sans perdre l'origine, on chaîne avec from :
try:
config = json.load(fichier)
except json.JSONDecodeError as e:
raise ConfigInvalideError("config.json est corrompu") from etry: config = json.load(fichier)except json.JSONDecodeError as e: raise ConfigInvalideError("config.json est corrompu") from eLa traceback affiche alors les deux exceptions, reliées par « The above exception was the direct cause of the following exception ». C'est l'équivalent de l'InnerException de C#.
# Exceptions personnalisées
Une exception métier est une simple classe qui hérite d'Exception. Souvent, une ligne suffit :
class ConfigInvalideError(Exception):
passclass ConfigInvalideError(Exception): passPour transporter du contexte, on ajoute des attributs et on passe le message au parent :
class SoldeInsuffisantError(Exception):
def __init__(self, demande, disponible):
super().__init__(f"Solde insuffisant : {demande} demandé, {disponible} disponible")
self.demande = demande
self.disponible = disponible
try:
compte.retirer(1000)
except SoldeInsuffisantError as e:
print(f"Il manque {e.demande - e.disponible} euros")class SoldeInsuffisantError(Exception): def __init__(self, demande, disponible): super().__init__(f"Solde insuffisant : {demande} demandé, {disponible} disponible") self.demande = demande self.disponible = disponible try: compte.retirer(1000)except SoldeInsuffisantError as e: print(f"Il manque {e.demande - e.disponible} euros") Par convention, le nom se termine par Error (ValueError, KeyError), pas par Exception comme en C#.
# EAFP : demander pardon plutôt que la permission
C'est le vrai changement de mentalité. Deux styles s'opposent :
- LBYL (Look Before You Leap) : on vérifie avant d'agir. Le style de C#.
- EAFP (Easier to Ask Forgiveness than Permission) : on tente, et on gère l'échec. Le style de Python.
# LBYL : on vérifie d'abord
if saisie.isdigit():
age = int(saisie)
else:
age = None
# EAFP : on tente
try:
age = int(saisie)
except ValueError:
age = None# LBYL : on vérifie d'abordif saisie.isdigit(): age = int(saisie)else: age = None # EAFP : on tentetry: age = int(saisie)except ValueError: age = NoneLa version EAFP est plus juste : isdigit() refuse "-5", que int() accepte très bien. La vérification préalable duplique la logique de conversion, en moins bien. Même chose pour les fichiers : tester qu'un fichier existe puis l'ouvrir laisse une fenêtre où il peut disparaître entre les deux, alors qu'ouvrir directement et rattraper FileNotFoundError est sans faille.
C'est l'exact opposé du conseil C# « n'utilise pas les exceptions pour le flow control ». En Python, le try est peu coûteux quand rien ne se passe, et le langage lui-même s'en sert : une boucle for s'arrête en interne quand l'itérateur lève StopIteration. EAFP va de pair avec le duck typing : on ne vérifie pas que l'objet sait faire, on essaie.
EAFP ne veut pas dire « des try partout ». Quand une méthode dédiée existe, elle reste la plus lisible : config.get("port", 3000) plutôt qu'un try/except KeyError.
# La hiérarchie des exceptions
BaseException
├── SystemExit sys.exit()
├── KeyboardInterrupt Ctrl+C
└── Exception tout ce qu'on rattrape normalement
├── ValueError
├── TypeError
├── AttributeError
├── LookupError
│ ├── KeyError
│ └── IndexError
├── ArithmeticError
│ └── ZeroDivisionError
└── OSError
└── FileNotFoundErrorBaseException├── SystemExit sys.exit()├── KeyboardInterrupt Ctrl+C└── Exception tout ce qu'on rattrape normalement ├── ValueError ├── TypeError ├── AttributeError ├── LookupError │ ├── KeyError │ └── IndexError ├── ArithmeticError │ └── ZeroDivisionError └── OSError └── FileNotFoundErrorRattraper un parent rattrape tous ses enfants : except LookupError couvre à la fois KeyError et IndexError. Et c'est parce que SystemExit et KeyboardInterrupt ne sont pas sous Exception qu'un except Exception: laisse quand même le programme s'arrêter proprement.
| Exception | Quand |
|---|---|
ValueError | Bon type, mauvaise valeur : int("abc") |
TypeError | Mauvais type : "1" + 2 |
AttributeError | Attribut ou méthode inexistant : None.upper() |
KeyError | Clé absente d'un dict |
IndexError | Index hors d'une liste |
ZeroDivisionError | Division par zéro |
FileNotFoundError | Fichier introuvable |
# La suite
- Les objets (POO) - les classes, pour écrire tes propres exceptions
- C# : Les exceptions - la même mécanique, avec la philosophie inverse sur le flow control
- Python : Introduction - le hub de la série
- Python : mon avis (mitigé) - la prise de recul sur le langage