Exceptions A : pourquoi gérer les erreurs, try et except

Objectifs de la leçon
  • Expliquer ce qu’est une Exception et ce qui arrive au programme quand elle n’est pas gérée
  • Écrire un bloc try / except qui attrape une erreur et la gère
  • Gérer séparément plusieurs types d’Exceptions, et obtenir les détails de l’erreur avec as
  • Comprendre pourquoi un except « vide » qui attrape tout est une habitude dangereuse

Qu’allons-nous apprendre ?

  • Ce qu’est une Exception.
  • Pourquoi un vrai logiciel doit gérer les erreurs et ne pas planter.
  • La syntaxe try / except.
  • Une gestion différente pour différents types d’erreurs.

Pourquoi en avons-nous besoin ?

Imaginez un petit serveur de calculatrice : un client envoie "hello" au lieu d’un nombre, et tout le serveur plante. Tous les clients qui viennent ensuite reçoivent « Connection refused ».

Dans le monde réel :

  • Un utilisateur tape « abc » dans un champ d’âge. L’application ne doit pas se fermer.
  • Un fichier que le programme essaie de lire a été supprimé. Il doit le signaler, pas planter.
  • Internet a coupé au milieu d’une requête. Il vaut la peine de réessayer, ou d’afficher un message.

Les erreurs arriveront : une mauvaise saisie, des fichiers manquants, un réseau instable. La seule question est ce que fait le logiciel quand elles arrivent.

Explication à partir des bases

Qu’est-ce qu’une Exception ?

Une Exception est un objet que Python « lance » (Raise) quand une Runtime Error se produit. Toutes les erreurs que nous avons vues hier (ValueError, ZeroDivisionError, TypeError…) sont des Exceptions.

Quand une Exception est lancée :

  1. Python arrête immédiatement la ligne en cours.
  2. Il cherche quelqu’un qui « attrapera » (Catch) l’erreur.
  3. Si personne ne l’attrape, le programme plante et un Traceback est affiché.

try / except est le moyen d’attraper.

Analogie : un filet de sécurité au cirque

Un funambule (votre code) peut tomber (une Exception). Sans filet, la chute est la fin du spectacle (un plantage). try = « le numéro se fait au-dessus d’un filet ». except = le filet, qui rattrape la chute, et ensuite vous décidez quoi faire : réessayer, prévenir le public, ou passer au numéro suivant.

Comment ça marche

  • try: — « essaie d’exécuter ce bloc ».
  • except SomeError: — « si une SomeError est lancée dans le try, saute ici et exécute ce bloc au lieu de planter ».
  • Si aucune erreur n’a été lancée, le bloc except est sauté.
  • Après le try/except, le programme continue normalement.

Syntaxe

try:
    # code that might fail
except ErrorType:
    # what to do if ErrorType happened

# several error types, each handled differently
try:
    ...
except ValueError:
    ...
except ZeroDivisionError:
    ...

# get the error object itself (its message)
except ValueError as error:
    print(error)

# same handling for several types
except (ValueError, TypeError):
    ...

Premier exemple de code

week-2/safe_age.py :

text = input("Enter your age: ")

try:
    age = int(text)
    print("Next year you will be", age + 1)
except ValueError:
    print("That is not a whole number:", text)

print("Program continues normally.")

Exécution 1 (saisie valide) :

Enter your age: 30
Next year you will be 31
Program continues normally.

Exécution 2 (saisie invalide) :

Enter your age: abc
That is not a whole number: abc
Program continues normally.

Analyse du code ligne par ligne

LigneExécution 1 (30)Exécution 2 (abc)
text = input(...)text = "30"text = "abc"
try:Entre dans le blocEntre dans le bloc
age = int(text)Réussit : age = 30Une ValueError est lancée, et Python saute directement au except
print("Next year...")AffichéSauté. On n’y arrive jamais
except ValueError:Sauté (pas d’erreur)Attrapée !
print("That is not...")Ne s’exécute pasAffiché
print("Program continues...")AffichéAffiché, et le programme n’a pas planté

Le point important : dès qu’une erreur est lancée, les lignes restantes du try ne s’exécutent pas. C’est pourquoi on ne met dans le try que ce qui dépend de la réussite.

Un autre exemple

Une calculatrice de division robuste, avec une gestion différente pour chaque type d’erreur et une boucle qui redemande. week-2/safe_divide.py :

def ask_number(prompt):
    while True:
        text = input(prompt)
        try:
            return float(text)
        except ValueError:
            print("Please type a number, for example 12 or 3.5")

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        print("Cannot divide by zero - returning 0")
        return 0

x = ask_number("First number: ")
y = ask_number("Second number: ")
print("Result:", divide(x, y))

Exemple d’exécution :

First number: ten
Please type a number, for example 12 or 3.5
First number: 10
Second number: 0
Cannot divide by zero - returning 0
Result: 0

Analyse :

  • return float(text) dans un try — si la conversion réussit, la fonction renvoie immédiatement et sort aussi de la boucle. Si elle échoue, except affiche un message, et la boucle redemande. Ainsi, on « oblige » l’utilisateur à une saisie valide.
  • Chaque fonction attrape l’erreur qui la concerne : ask_number attrape ValueError (la saisie), divide attrape ZeroDivisionError (le calcul).
  • Une question ouverte : divide doit-elle renvoyer 0 en cas de division par zéro ? Mathématiquement, c’est une réponse fausse ! Dans bien des cas, il vaut mieux ne pas l’attraper ici, et laisser l’appelant décider. Nous en parlerons dans la leçon sur raise.

as error — obtenir les détails de l’erreur

try:
    number = int("12.5")
except ValueError as error:
    print("Conversion failed:", error)
Conversion failed: invalid literal for int() with base 10: '12.5'

error est l’objet de l’erreur. L’afficher montre le message qui serait apparu à la dernière ligne du Traceback.

Erreur courante

Erreur 1 : un except vide qui attrape tout.

try:
    age = int(input("Age: "))
    print("Double:", age * 2)
    reslt = age / 0
except:
    print("Something went wrong")

Il y a ici trois problèmes différents (la saisie, la division par zéro, et même une lettre manquante dans un nom de variable), et tous sont cachés sous « Something went wrong ». Impossible à déboguer. Et pire encore : un except: vide attrape aussi KeyboardInterrupt, donc même Ctrl + C n’arrêtera pas un programme coincé dans une boucle comme celle-ci !

La règle : n’attrapez que des erreurs précises que vous savez gérer. Toutes les autres continuent de remonter et font planter, et c’est une bonne chose, car c’est ainsi qu’on découvre les bugs.

Erreur 2 : un try énorme.

try:
    # 40 lines of code...
except ValueError:
    print("Invalid value")

Laquelle des 40 lignes a échoué ? Impossible à savoir. Ne mettez dans le try que les lignes qui peuvent échouer avec l’erreur que vous attrapez, le moins possible.

Erreur 3 : « avaler » une erreur en silence.

try:
    price = float(text)
except ValueError:
    pass

pass signifie « ne rien faire ». L’erreur disparaît, et price n’existe peut-être même pas, ce qui provoquera une NameError déroutante quelques lignes plus loin. Faites toujours quelque chose : un message, une valeur par défaut expliquée, ou une écriture dans un journal.

Exercices

Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.

Exercice 1: Conversion sûre en nombre

Écrivez une fonction to_number(text) qui renvoie le texte sous forme de nombre entier, et si le texte n’est pas un nombre — renvoie None, sans que le programme plante.

print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))

Résultat attendu :

42
None
7
Afficher la solution
def to_number(text):
    try:
        return int(text)
    except ValueError:
        return None


print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))
42
None
7

int("hello") lance une ValueError. Sans try, le programme planterait. Avec except ValueError, nous décidons de ce qui se passe à la place.

Exercice 2: Une gestion différente pour chaque type d’erreur

Écrivez une fonction safe_divide(a_text, b_text) qui reçoit deux textes, les convertit en nombres et divise. Elle renvoie :

  • le résultat de la division, si tout est valide ;
  • "numbers only" si l’un des textes n’est pas un nombre ;
  • "cannot divide by zero" si le diviseur vaut 0.
print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))

Résultat attendu :

2.5
cannot divide by zero
numbers only
Afficher la solution
def safe_divide(a_text, b_text):
    try:
        return int(a_text) / int(b_text)
    except ValueError:
        return "numbers only"
    except ZeroDivisionError:
        return "cannot divide by zero"


print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))
2.5
cannot divide by zero
numbers only

Chaque type d’erreur a son propre except, avec une gestion adaptée. Python vérifie les blocs except dans l’ordre et entre dans le premier qui correspond.

Exercice 3: Afficher le message d’erreur

Le code suivant échoue avec une ValueError. Enveloppez-le dans un try, attrapez l’erreur avec except ValueError as error, et affichez le texte Could not convert: suivi du message que Python a enregistré dans error :

number = int("12abc")

Résultat attendu :

Could not convert: invalid literal for int() with base 10: '12abc'
Afficher la solution
try:
    number = int("12abc")
except ValueError as error:
    print("Could not convert:", error)
Could not convert: invalid literal for int() with base 10: '12abc'

as error enregistre l’objet de l’erreur dans une variable. Quand on l’affiche, on obtient le message créé par Python.

Exercice 4: Choisir dans une liste

Vous avez une liste de noms. Écrivez une fonction pick(names, position_text) qui reçoit une position sous forme de texte (en commençant à 1) et renvoie le nom à cette position. Elle renvoie "not a number" pour une saisie qui n’est pas un nombre, et "no such position" pour une position qui n’existe pas.

names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))

Résultat attendu :

Avi
not a number
no such position
Afficher la solution
def pick(names, position_text):
    try:
        position = int(position_text)
        return names[position - 1]
    except ValueError:
        return "not a number"
    except IndexError:
        return "no such position"


names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))
Avi
not a number
no such position

Deux erreurs différentes à deux endroits : int(...) peut lancer une ValueError, et l’accès à la liste peut lancer une IndexError.

Exercice 5: Un except qui attrape tout

def read_age(text):
    try:
        return int(text)
    except:
        return 0
  1. Pourquoi un except: sans type d’erreur est-il dangereux ?
  2. Donnez un exemple de vrai bug que ce code cacherait.
  3. Corrigez le code.
Afficher la solution
  1. Un except: vide attrape toutes les erreurs — y compris celles auxquelles nous n’avons pas pensé — et les cache. Le code continue comme si tout allait bien, avec une valeur fausse.
  2. Si quelqu’un passe None par erreur au lieu d’un texte, une TypeError est lancée — un vrai bug dans le code qui a appelé la fonction. Au lieu de le voir, on obtient silencieusement l’âge 0. (Même Ctrl + C est attrapé par un except: vide.)
  3. N’attrapez que l’erreur attendue :
def read_age(text):
    try:
        return int(text)
    except ValueError:
        return 0


print(read_age("30"))
print(read_age("thirty"))
30
0

Résumé

  • Exception = une erreur que Python lance à l’exécution. Si personne ne l’attrape, le programme plante.
  • try = « essaie ». except SomeError = « si cette erreur s’est produite, fais ceci au lieu de planter ».
  • Dès qu’une erreur est lancée, le reste du try est sauté.
  • except SomeError as error donne accès au message d’erreur.
  • N’attrapez que des erreurs précises, avec un try petit. Pas de except: vide ni de pass qui avale les erreurs.

Documentation officielle