Initialisation des systèmes...

Baptiste.Dev
Retour aux notes
LangagesIntermédiaireSérie : Python

Python : Les exceptions

try / except / else / finally, with, raise, exceptions personnalisées et le style EAFP : gérer les erreurs à la Python.

6 min de lecture
Mis à jour aujourd'hui
pythonexceptionstryexceptraiseeafperreurs

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]
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    nombres[10]
IndexError: list index out of range

Se 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}")

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")

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 cas
  • else s'exécute seulement si le try n'a levé aucune exception. Il n'existe pas en C#. Son intérêt : garder le try minimal. Ici, si enregistrer() lève à son tour une ValueError, elle ne sera pas prise à tort pour une « saisie invalide ».
  • finally s'exécute toujours, exception ou pas, même après un return. 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 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 -= montant

Dans 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 quelle

Pour 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 e

La 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):
    pass

Pour 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")

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

La 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
        └── FileNotFoundError

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

ExceptionQuand
ValueErrorBon type, mauvaise valeur : int("abc")
TypeErrorMauvais type : "1" + 2
AttributeErrorAttribut ou méthode inexistant : None.upper()
KeyErrorClé absente d'un dict
IndexErrorIndex hors d'une liste
ZeroDivisionErrorDivision par zéro
FileNotFoundErrorFichier introuvable

# La suite