35  Gestion des exceptions

Un programme réel rencontre des imprévus : fichier manquant, saisie invalide, réseau coupé, division par zéro… La gestion des exceptions est le mécanisme Python pour réagir proprement à ces situations sans planter. C’est un savoir-faire essentiel dès le niveau Avancé du TOSA.

35.1 Qu’est-ce qu’une exception ?

Quand Python rencontre une erreur à l’exécution, il « lève » (raises) une exception. Sans gestion, le programme s’arrête immédiatement.

n = int("abc")        # ValueError
print("Suite...")     # jamais atteint
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[1], line 1
----> 1 n = int("abc")        # ValueError
      2 print("Suite...")     # jamais atteint

ValueError: invalid literal for int() with base 10: 'abc'

Chaque erreur appartient à un type (ValueError, TypeError, ZeroDivisionError…) et contient un message.

Les exceptions les plus courantes

Exception Cause typique
SyntaxError Erreur de syntaxe (détectée avant exécution)
NameError Variable non définie
TypeError Opération entre types incompatibles
ValueError Type correct, valeur invalide (int("abc"))
ZeroDivisionError Division par zéro
IndexError Indice hors limites dans une séquence
KeyError Clé absente dans un dict
FileNotFoundError Fichier introuvable
AttributeError Attribut inexistant sur un objet
ImportError / ModuleNotFoundError Module introuvable

35.2 try / except : intercepter une exception

La structure de base :

try:
    # code risqué
except TypeErreur:
    # que faire si TypeErreur arrive

Premier exemple

try:
    n = int("abc")
except ValueError:
    print("Ce n'est pas un entier valide !")

print("Le programme continue.")
Ce n'est pas un entier valide !
Le programme continue.

Le programme ne plante pas. Python saute dans le except, puis reprend son cours.

Récupérer le message de l’erreur

Avec as e on récupère l’objet exception, notamment pour accéder à son message.

try:
    n = int("abc")
except ValueError as e:
    print(f"Erreur : {e}")
Erreur : invalid literal for int() with base 10: 'abc'

Attraper plusieurs types différents

valeurs = ["10", "abc", None]

for v in valeurs:
    try:
        n = int(v)
        print(f"OK : {n}")
    except ValueError:
        print(f"Valeur invalide : {v!r}")
    except TypeError:
        print(f"Type invalide : {v!r}")
OK : 10
Valeur invalide : 'abc'
Type invalide : None

On peut aussi grouper plusieurs exceptions en un seul except avec un tuple :

valeurs = ["10", "abc", None]

for v in valeurs:
    try:
        n = int(v)
        print(f"OK : {n}")
    except (ValueError, TypeError) as e:
        print(f"Problème avec {v!r} : {e}")
OK : 10
Problème avec 'abc' : invalid literal for int() with base 10: 'abc'
Problème avec None : int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
⚠️ Évitez except: tout seul

L’exception « attrape-tout » (except: sans type) ou except Exception: attrape même les bugs et masque les vrais problèmes.

# ❌ Mauvaise pratique
try:
    traiter()
except:            # attrape même Ctrl+C !
    pass

Règle d’or : soyez spécifique. Attrapez les exceptions que vous savez traiter. Laissez les autres remonter (c’est ainsi qu’on découvre les bugs).

35.3 Le pattern EAFP vs LBYL

Python a une philosophie particulière : « il est plus facile de demander pardon que la permission » (Easier to Ask Forgiveness than Permission, EAFP).

LBYL : « Look Before You Leap »

Vérifier avant d’agir — typique des autres langages.

d = {"a": 1, "b": 2}

# LBYL : on vérifie avant
if "c" in d:
    print(d["c"])
else:
    print("Clé absente")
Clé absente

EAFP : « Easier to Ask Forgiveness »

Essayer, et rattraper en cas d’erreur.

d = {"a": 1, "b": 2}

# EAFP : on tente, on rattrape
try:
    print(d["c"])
except KeyError:
    print("Clé absente")
Clé absente
Laquelle préférer ?
  • LBYL : plus lisible pour des cas simples, idiomatique avec dict.get().
  • EAFP : plus sûr dans des environnements concurrents (pas de « race condition » entre le test et l’action), et généralement plus Pythonique.

Pour les dicts, d.get("c", "défaut") est souvent le plus concis.

35.4 else et finally

Un bloc try peut inclure jusqu’à 4 clauses : try, except, else, finally.

else — exécuté si aucune exception n’a été levée

def diviser(a, b):
    try:
        resultat = a / b
    except ZeroDivisionError:
        print("Division par zéro !")
    else:
        print(f"Résultat : {resultat}")

diviser(10, 2)       # else exécuté
diviser(10, 0)       # except exécuté, else non
Résultat : 5.0
Division par zéro !

Pourquoi un else ? Pour séparer clairement ce qu’on protège (dans try) de ce qu’on fait ensuite (dans else). Évite d’attraper accidentellement une exception venue du code post-opération.

finally — toujours exécuté

def diviser(a, b):
    try:
        resultat = a / b
    except ZeroDivisionError:
        print("Division par zéro")
        return None
    else:
        return resultat
    finally:
        print("Nettoyage (toujours exécuté)")

print(diviser(10, 2))
print("---")
print(diviser(10, 0))
Nettoyage (toujours exécuté)
5.0
---
Division par zéro
Nettoyage (toujours exécuté)
None

finally sert au nettoyage : fermer un fichier, libérer une ressource, déconnecter une base de données… Il s’exécute quoi qu’il arrive, même en cas de return ou d’exception non attrapée.

Structure complète

try:
    # code risqué
except TypeErreur1:
    # gestion spécifique
except TypeErreur2:
    # autre gestion
else:
    # si aucune exception n'est levée
finally:
    # toujours exécuté

35.5 Lever une exception : raise

Vous pouvez vous-même lever une exception avec raise.

def diviser(a, b):
    if b == 0:
        raise ValueError("Diviseur nul")
    return a / b

try:
    print(diviser(10, 0))
except ValueError as e:
    print(f"Erreur : {e}")
Erreur : Diviseur nul

Quand raise est-il utile ?

  • Pour signaler un cas invalide dès qu’il est détecté.
  • Dans des fonctions de validation.
  • Dans des setters @property (vu au chapitre précédent).
  • Pour re-lever une exception après l’avoir loguée.
def age_valide(age):
    if not isinstance(age, int):
        raise TypeError("L'âge doit être un entier")
    if age < 0:
        raise ValueError("L'âge ne peut pas être négatif")
    if age > 150:
        raise ValueError("L'âge est irréaliste")
    return age

# Tests
try:
    age_valide(-5)
except ValueError as e:
    print(f"Refusé : {e}")

try:
    age_valide("vingt")
except TypeError as e:
    print(f"Refusé : {e}")

print(age_valide(30))   # OK
Refusé : L'âge ne peut pas être négatif
Refusé : L'âge doit être un entier
30

Re-lever une exception

Parfois, on veut traiter une exception (logger, nettoyer) puis la re-lever pour que le code appelant la gère aussi.

def traiter(valeur):
    try:
        return 10 / valeur
    except ZeroDivisionError as e:
        print(f"Log : erreur détectée ({e})")
        raise               # re-lève l'exception courante

traiter(0)
Log : erreur détectée (division by zero)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[12], line 8
      4     except ZeroDivisionError as e:
      5         print(f"Log : erreur détectée ({e})")
      6         raise               # re-lève l'exception courante
      7 
----> 8 traiter(0)

Cell In[12], line 6, in traiter(valeur)
      2     try:
      3         return 10 / valeur
      4     except ZeroDivisionError as e:
      5         print(f"Log : erreur détectée ({e})")
----> 6         raise               # re-lève l'exception courante

ZeroDivisionError: division by zero

35.6 Créer ses propres exceptions

On peut définir ses propres classes d’exception pour donner plus de sens à son code. Il suffit d’hériter de Exception (ou d’une de ses sous-classes).

class SoldeInsuffisantError(Exception):
    """Levée quand un retrait dépasse le solde disponible."""
    pass


class CompteBancaire:
    def __init__(self, titulaire, solde=0):
        self.titulaire = titulaire
        self.solde = solde

    def retirer(self, montant):
        if montant > self.solde:
            raise SoldeInsuffisantError(
                f"Solde insuffisant : demande {montant} € mais solde = {self.solde} €"
            )
        self.solde -= montant


compte = CompteBancaire("Alice", 100)

try:
    compte.retirer(500)
except SoldeInsuffisantError as e:
    print(f"Opération refusée : {e}")
Opération refusée : Solde insuffisant : demande 500 € mais solde = 100 €

Avantages des exceptions personnalisées

  • Expressivité : SoldeInsuffisantError est plus clair que Exception ou ValueError.
  • Granularité : on peut rattraper spécifiquement ses propres erreurs, sans confondre avec d’autres.
  • Documentation : le nom de l’exception raconte le bug.
Convention de nommage

Les classes d’exception doivent finir par Error (ou Exception). C’est la convention PEP 8 :

  • SoldeInsuffisantError, UtilisateurInconnuError
  • SoldeInsuffisant (ambigu)

35.7 Le context manager with — aperçu

Pour garantir que les ressources sont proprement libérées (même en cas d’erreur), Python propose l’instruction with.

with open("fichier.txt") as f:
    contenu = f.read()
    # ... traitements
# Le fichier est AUTOMATIQUEMENT fermé à la sortie du bloc,
# même si une exception a été levée dans le bloc.

Équivalent (verbeux) avec try/finally :

f = open("fichier.txt")
try:
    contenu = f.read()
    # ...
finally:
    f.close()

with est toujours préféré pour les fichiers, connexions, verrous, etc. Nous le verrons en détail au chapitre suivant sur les fichiers.

35.8 Arbre d’héritage des exceptions

Les exceptions forment une hiérarchie. En rattraper une rattrape aussi toutes ses sous-classes.

BaseException
 └── Exception
      ├── ArithmeticError
      │    ├── ZeroDivisionError
      │    └── OverflowError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── OSError
      │    └── FileNotFoundError
      ├── TypeError
      ├── ValueError
      └── ... etc.

Implications pratiques :

try:
    d = {"a": 1}
    d["b"]
except LookupError as e:       # attrape KeyError ET IndexError
    print(f"Erreur de lookup : {e}")
Erreur de lookup : 'b'
try:
    1 / 0
except Exception as e:          # attrape PRESQUE tout
    print(f"Quelque chose s'est mal passé : {e}")
Quelque chose s'est mal passé : division by zero
Ne pas confondre Exception et BaseException
  • Exception : toutes les erreurs « normales ». À attraper si besoin.
  • BaseException : inclut aussi KeyboardInterrupt (Ctrl+C) et SystemExit. Ne l’attrapez jamais sauf raison très précise.

35.9 Assertions : assert

assert est un outil de vérification interne au programme. Si la condition est fausse, une AssertionError est levée.

def moyenne(notes):
    assert len(notes) > 0, "La liste de notes ne peut pas être vide"
    return sum(notes) / len(notes)

print(moyenne([12, 15, 14]))
13.666666666666666
moyenne([])
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[17], line 1
----> 1 moyenne([])

Cell In[16], line 2, in moyenne(notes)
      1 def moyenne(notes):
----> 2     assert len(notes) > 0, "La liste de notes ne peut pas être vide"
      3     return sum(notes) / len(notes)

AssertionError: La liste de notes ne peut pas être vide
assert n’est PAS pour la validation d’entrées

Les assertions peuvent être désactivées avec l’option -O (optimize) de Python. Ne les utilisez que pour des vérifications internes que vous êtes sûrs d’être toujours vraies.

Mauvais usage :

def retirer(montant, solde):
    assert montant > 0, "Montant invalide"    # ❌ disparaît avec -O

Bon usage :

def retirer(montant, solde):
    if montant <= 0:
        raise ValueError("Montant invalide")   # ✅ toujours actif

🧩 Quiz 6.1 — Gestion des exceptions

Question 1

Quelle exception est levée par int("abc") ?

  1. TypeError
  2. ValueError
  3. SyntaxError
  4. NameError

b) ValueError — le type (str) est accepté, mais la valeur ne représente pas un entier. TypeError serait pour int([1, 2]) (mauvais type).

int("abc")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[18], line 1
----> 1 int("abc")

ValueError: invalid literal for int() with base 10: 'abc'

Question 2

Quelle clause s’exécute si aucune exception n’est levée dans try ?

  1. except
  2. else
  3. finally
  4. raise

b) else — exécuté uniquement si le bloc try s’est déroulé sans aucune exception.

try:
    x = 10
except Exception:
    print("erreur")
else:
    print("OK")
OK

Question 3

Quelle clause s’exécute quoi qu’il arrive ?

  1. try
  2. except
  3. else
  4. finally

d) finally — toujours exécuté, même en cas de return anticipé ou d’exception non rattrapée. Idéal pour le nettoyage.

Question 4

Que s’affiche-t-il ?

try:
    x = 10 / 0
except ZeroDivisionError:
    x = 0
print(x)
  1. 10
  2. 0
  3. Une erreur
  4. None

b) 0 — la division lève ZeroDivisionError, qui est attrapée, x prend la valeur 0.

try:
    x = 10 / 0
except ZeroDivisionError:
    x = 0
print(x)
0

Question 5

Que fait raise ValueError("msg") ?

  1. Affiche le message
  2. Arrête le programme
  3. Lève une exception ValueError avec le message
  4. Définit une variable nommée ValueError

c)raise lève une exception. Si elle n’est pas rattrapée, le programme s’arrête ; si elle est dans un try/except, elle peut être gérée.

Question 6

Comment attraper à la fois ValueError et TypeError ?

  1. except ValueError and TypeError:
  2. except ValueError + TypeError:
  3. except (ValueError, TypeError):
  4. except ValueError or TypeError:

c) except (ValueError, TypeError): — tuple d’exceptions entre parenthèses.

Question 7

Quelle est la différence entre Exception et BaseException ?

  1. Aucune, ce sont des synonymes
  2. BaseException inclut KeyboardInterrupt et SystemExit, pas Exception
  3. Exception est plus large
  4. BaseException n’existe pas

b)BaseException est la racine de la hiérarchie. Attraper BaseException empêcherait notamment l’utilisateur de quitter avec Ctrl+C. Utilisez Exception pour les rattrapages larges.

Question 8

Pourquoi except: tout seul (sans type) est déconseillé ?

  1. C’est une erreur de syntaxe
  2. Il attrape même les bugs que vous ne pouvez pas gérer et les masque
  3. Il ne fonctionne qu’en Python 2
  4. Il est plus lent

b)except: attrape toutes les exceptions, y compris KeyboardInterrupt, SystemExit, et les bugs de programmation. Cela masque les vrais problèmes et rend le débogage très difficile. Soyez spécifique dans vos except.


✏️ Exercice 6.1 — Conversion sécurisée

Écrivez une fonction convertir_int(chaine, defaut=0) qui convertit une chaîne en entier, et renvoie defaut si la conversion échoue.

def convertir_int(chaine, defaut=0):
    ...

print(convertir_int("42"))         # 42
print(convertir_int("abc"))        # 0
print(convertir_int("abc", -1))    # -1
print(convertir_int("3.14", 0))    # 0 (pas un int)
def convertir_int(chaine, defaut=0):
    try:
        return int(chaine)
    except (ValueError, TypeError):
        return defaut

print(convertir_int("42"))
print(convertir_int("abc"))
print(convertir_int("abc", -1))
print(convertir_int("3.14", 0))
print(convertir_int(None, -999))
42
0
-1
0
-999
  • On rattrape à la fois ValueError (chaîne non convertible) et TypeError (type invalide comme None).
  • Ce pattern « fonction sécurisée avec défaut » est très courant dans le code utilitaire.

✏️ Exercice 6.2 — Saisie robuste

Écrivez une fonction demander_note() qui redemande la note tant qu’elle n’est pas un entier entre 0 et 20.

def demander_note():
    while True:
        saisie = input("Note (0-20) : ")
        try:
            ...
        except ValueError:
            ...

note = demander_note()
print(f"Note enregistrée : {note}")
def demander_note():
    """Demande une note entière entre 0 et 20, redemande si invalide."""
    while True:
        saisie = input("Note (0-20) : ")
        try:
            note = int(saisie)
        except ValueError:
            print("  Ce n'est pas un entier, réessayez.")
            continue

        if 0 <= note <= 20:
            return note
        print("  La note doit être entre 0 et 20, réessayez.")


# Démo interactive : note = demander_note()
def verifier_note(saisie):
    """Version testable sans input()."""
    try:
        note = int(saisie)
    except ValueError:
        return None, "Pas un entier"

    if not 0 <= note <= 20:
        return None, "Hors bornes"

    return note, "OK"


for s in ["abc", "-5", "21", "12", "20"]:
    note, msg = verifier_note(s)
    print(f"'{s}' → {note} ({msg})")
'abc' → None (Pas un entier)
'-5' → None (Hors bornes)
'21' → None (Hors bornes)
'12' → 12 (OK)
'20' → 20 (OK)

✏️ Exercice 6.3 — Exception personnalisée

Créez une classe AgeInvalideError et une fonction calculer_annee_naissance(age) qui lève cette exception si l’âge est négatif ou > 150. Sinon, renvoie l’année de naissance (année courante - âge).

from datetime import date

class AgeInvalideError(Exception):
    ...

def calculer_annee_naissance(age):
    ...

# Tests
try:
    print(calculer_annee_naissance(30))
    print(calculer_annee_naissance(-5))
except AgeInvalideError as e:
    print(f"Erreur : {e}")
from datetime import date

class AgeInvalideError(Exception):
    """Levée quand un âge est hors de la plage plausible."""
    pass


def calculer_annee_naissance(age):
    if age < 0:
        raise AgeInvalideError(f"Âge négatif : {age}")
    if age > 150:
        raise AgeInvalideError(f"Âge irréaliste : {age}")
    return date.today().year - age


# Tests
for age in [30, -5, 200, 80]:
    try:
        an = calculer_annee_naissance(age)
        print(f"  Âge {age} → né en {an}")
    except AgeInvalideError as e:
        print(f"  Refus : {e}")
  Âge 30 → né en 1996
  Refus : Âge négatif : -5
  Refus : Âge irréaliste : 200
  Âge 80 → né en 1946

✏️ Exercice 6.4 — finally pour garantir un nettoyage

Simulez une connexion à une base de données avec try/finally. Même en cas d’erreur pendant le traitement, la connexion doit être « fermée ».

def traiter_avec_connexion(donnees):
    print("Connexion à la BD...")
    connecte = True

    try:
        # Traitement potentiellement risqué
        ...
    finally:
        # Fermer la connexion — TOUJOURS
        ...

traiter_avec_connexion([1, 2, 3])
traiter_avec_connexion([])         # doit quand même fermer
def traiter_avec_connexion(donnees):
    print("  → Connexion à la BD établie")

    try:
        if not donnees:
            raise ValueError("Aucune donnée à traiter")
        total = sum(donnees)
        print(f"  → Total calculé : {total}")
    except ValueError as e:
        print(f"  ⚠️  Erreur de traitement : {e}")
    finally:
        print("  ← Connexion à la BD fermée")


print("Cas 1 : données OK")
traiter_avec_connexion([10, 20, 30])

print("\nCas 2 : données vides")
traiter_avec_connexion([])
Cas 1 : données OK
  → Connexion à la BD établie
  → Total calculé : 60
  ← Connexion à la BD fermée

Cas 2 : données vides
  → Connexion à la BD établie
  ⚠️  Erreur de traitement : Aucune donnée à traiter
  ← Connexion à la BD fermée

Dans le cas 2, même si une exception a eu lieu, finally s’est bien exécuté et a fermé la connexion.

En Python moderne, on préfère with pour ce genre de gestion (vu au chapitre suivant) :

with get_connexion() as conn:
    conn.traiter(donnees)
# Fermeture automatique, même en cas d'exception

À retenir

Points clés du chapitre
  1. Exceptions : mécanisme Python pour signaler/gérer des situations anormales sans planter.
  2. try / except : code risqué / gestion d’erreur. except SpecificError as e: pour accéder au message.
  3. Soyez spécifique dans vos except — ne rattrapez pas Exception sans raison, jamais BaseException.
  4. else : exécuté si aucune exception. finally : toujours exécuté (nettoyage).
  5. raise pour lever vos propres exceptions. Convention : nom finissant par Error.
  6. EAFP (try/except) est souvent préféré à LBYL (if then) en Python.
  7. Créez des exceptions personnalisées pour expressivité — héritez de Exception.
  8. assert sert aux vérifications internes (pas à la validation d’entrées).
  9. with (chapitre suivant) est la manière moderne de garantir le nettoyage.

← Chapitre précédent : POO (2/2)Chapitre suivant : Fichiers →