Leçon 8/9 8 min

La gestion des erreurs

try/except/finally, les exceptions courantes (ValueError, FileNotFoundError, KeyError), et lever ses propres erreurs avec raise.

FR EN

Pourquoi gérer les erreurs

Un programme qui plante au premier imprévu est inutilisable. Un utilisateur tape abc au lieu d'un nombre, un fichier attendu n'existe pas, une clé manque dans un dictionnaire… Dans la vraie vie, l'imprévu est permanent. Sans précaution, votre programme s'arrête net.

Quand quelque chose tourne mal, Python lève une exception : il interrompt l'exécution à l'instant précis du problème et fait remonter l'erreur. Si personne ne l'attrape, elle remonte jusqu'en haut du programme, qui s'arrête en affichant une traceback.

Regardez ce code planter : on demande à Python de convertir "abc" en entier, ce qui est impossible.

nombre = int("abc")
print(nombre)

Python lève une ValueError et la ligne print n'est jamais atteinte. Voici la version protégée : on entoure l'opération risquée d'un bloc try et on prévoit quoi faire si elle échoue.

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

print("Le programme continue normalement.")

Cette fois le programme ne plante plus : il affiche le message d'erreur, puis continue.

Sans try, l'erreur stoppe le programme. Avec try/except, elle est attrapée et le programme continue. Sans try code risqué PLANTAGE le programme s'arrête Avec try / except code risqué except attrape le programme continue
Sans protection, l'exception stoppe tout. Avec try/except, elle est attrapée et le programme reprend son cours.

try / except : attraper une erreur précise

Le bloc try contient le code qui peut échouer. Le bloc except indique quoi faire si une exception d'un type donné survient. On vise toujours un type précis :

try:
    age = int(input("Votre âge : "))
    print(f"Vous avez {age} ans")
except ValueError:
    print("Veuillez entrer un nombre entier.")

On peut attraper plusieurs types différents, soit avec plusieurs blocs except, soit en regroupant les types dans un tuple :

donnees = {"nom": "Alice"}

try:
    valeur = donnees["age"] + 1
except KeyError:
    print("La clé 'age' n'existe pas.")
except TypeError:
    print("Type incompatible pour l'addition.")

# Ou regrouper plusieurs types d'un coup
try:
    valeur = donnees["age"] + 1
except (KeyError, TypeError):
    print("Donnée manquante ou de mauvais type.")

Vous pouvez aussi récupérer l'objet exception avec as pour afficher le message exact :

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

Exceptions courantes : ValueError (bonne type, mauvaise valeur, ex. int("abc")), FileNotFoundError (fichier introuvable), KeyError (clé absente d'un dictionnaire), TypeError (opération entre types incompatibles), ZeroDivisionError (division par zéro).

Attention : ne faites jamais de except: nu (sans préciser de type). Il attrape tout, y compris les bugs de votre code et les interruptions clavier. Vous masquez alors des erreurs que vous auriez dû voir, et le débogage devient un cauchemar. Visez toujours le type d'exception que vous savez gérer.

else et finally

Deux blocs optionnels complètent try/except :

  • else s'exécute seulement si aucune erreur n'a été levée dans le try.
  • finally s'exécute toujours, qu'il y ait eu une erreur ou non. C'est le bloc de nettoyage : fermer un fichier, une connexion, libérer une ressource.
def diviser(a, b):
    try:
        resultat = a / b
    except ZeroDivisionError:
        print("Division par zéro impossible.")
    else:
        print(f"Résultat : {resultat}")
    finally:
        print("Calcul terminé.")

diviser(10, 2)
print("---")
diviser(10, 0)

Résultat : pour diviser(10, 2), le else affiche le résultat ; pour diviser(10, 0), le except affiche l'erreur. Dans les deux cas, le finally affiche « Calcul terminé ».

Bonne pratique : placez dans finally tout ce qui doit s'exécuter quoi qu'il arrive (fermeture de fichier, déconnexion). Et gardez le bloc try le plus court possible : seulement la ligne qui peut réellement échouer.

raise : lever sa propre exception

Jusqu'ici, c'est Python qui levait les erreurs. Mais vous pouvez aussi lever les vôtres avec raise, pour signaler qu'une situation est anormale dans votre code. C'est bien plus clair qu'un résultat faux qui se propage en silence.

def calculer_remise(prix, pourcentage):
    if pourcentage < 0 or pourcentage > 100:
        raise ValueError("Le pourcentage doit être entre 0 et 100.")
    return prix * (1 - pourcentage / 100)

# Usage normal
print(calculer_remise(100, 20))  # 80.0

# Valeur aberrante : on lève une erreur explicite
print(calculer_remise(100, 150))

L'appel avec 150 lève une ValueError avec votre message. Celui qui utilise votre fonction peut alors l'attraper avec try/except :

try:
    calculer_remise(100, 150)
except ValueError as e:
    print(f"Remise refusée : {e}")  # Remise refusée : Le pourcentage doit être entre 0 et 100.

Note : choisissez le type d'exception le plus parlant. ValueError pour une valeur invalide, TypeError pour un mauvais type, ou une exception personnalisée (créée avec class MonErreur(Exception): pass) pour les cas vraiment spécifiques à votre application.

assert : vérifier une hypothèse

L'instruction assert vérifie qu'une condition est vraie. Si elle est fausse, Python lève une AssertionError avec le message que vous fournissez. La syntaxe est : assert condition, "message".

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

print(moyenne([12, 15, 18]))  # 15.0

# Cette hypothèse est fausse : assert lève une AssertionError
print(moyenne([]))

C'est très pratique pour vérifier une hypothèse pendant le développement, pour déboguer, ou dans des tests (assert resultat == 42).

Attention : les assert peuvent être désactivés quand Python est lancé en mode optimisé (python -O) : ils sont alors purement ignorés. Ne les utilisez donc jamais pour valider des entrées utilisateur en production. Pour ça, utilisez un vrai if suivi d'un raise.

Récap

  • Une exception interrompt le programme ; sans protection, il s'arrête.
  • try entoure le code risqué, except TypeError attrape un type précis.
  • On regroupe plusieurs types avec except (KeyError, TypeError) et on récupère le message avec as e.
  • Jamais de except: nu : il masque les vrais bugs.
  • else tourne si tout s'est bien passé ; finally tourne toujours (nettoyage).
  • raise lève votre propre exception pour signaler une anomalie dans votre code.

Why handle errors

A program that crashes at the first surprise is unusable. A user types abc instead of a number, an expected file does not exist, a key is missing from a dictionary… In real life, the unexpected is constant. Without precautions, your program stops dead.

When something goes wrong, Python raises an exception: it interrupts execution at the exact point of the problem and lets the error bubble up. If nobody catches it, it rises to the top of the program, which stops and prints a traceback.

Watch this code crash: we ask Python to convert "abc" to an integer, which is impossible.

number = int("abc")
print(number)

Python raises a ValueError and the print line is never reached. Here is the protected version: we wrap the risky operation in a try block and decide what to do if it fails.

try:
    number = int("abc")
    print(number)
except ValueError:
    print("This is not a valid number.")

print("The program keeps running normally.")

This time the program no longer crashes: it prints the error message, then continues.

Without try, the error stops the program. With try/except, it is caught and the program continues. Without try risky code CRASH the program stops With try / except risky code except catches program continues
Without protection, the exception stops everything. With try/except, it is caught and the program resumes.

try / except: catching a specific error

The try block contains the code that may fail. The except block says what to do if an exception of a given type occurs. Always target a specific type:

try:
    age = int(input("Your age: "))
    print(f"You are {age} years old")
except ValueError:
    print("Please enter a whole number.")

You can catch several different types, either with multiple except blocks, or by grouping the types in a tuple:

data = {"name": "Alice"}

try:
    value = data["age"] + 1
except KeyError:
    print("The 'age' key does not exist.")
except TypeError:
    print("Incompatible type for the addition.")

# Or group several types at once
try:
    value = data["age"] + 1
except (KeyError, TypeError):
    print("Missing data or wrong type.")

You can also grab the exception object with as to display the exact message:

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

Common exceptions: ValueError (right type, wrong value, e.g. int("abc")), FileNotFoundError (file not found), KeyError (key missing from a dictionary), TypeError (operation between incompatible types), ZeroDivisionError (division by zero).

Warning: never use a bare except: (without specifying a type). It catches everything, including bugs in your own code and keyboard interrupts. You then hide errors you should have seen, and debugging becomes a nightmare. Always target the exception type you know how to handle.

else and finally

Two optional blocks complete try/except:

  • else runs only if no error was raised in the try.
  • finally always runs, whether there was an error or not. It is the cleanup block: closing a file, a connection, releasing a resource.
def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("Division by zero is impossible.")
    else:
        print(f"Result: {result}")
    finally:
        print("Computation done.")

divide(10, 2)
print("---")
divide(10, 0)

Result: for divide(10, 2), the else prints the result; for divide(10, 0), the except prints the error. In both cases, the finally prints "Computation done."

Best practice: put in finally anything that must run no matter what (closing a file, disconnecting). And keep the try block as short as possible: only the line that can actually fail.

raise: raising your own exception

So far, Python raised the errors. But you can also raise your own with raise, to signal that a situation is abnormal in your code. It is far clearer than a wrong result that silently spreads.

def compute_discount(price, percent):
    if percent < 0 or percent > 100:
        raise ValueError("Percent must be between 0 and 100.")
    return price * (1 - percent / 100)

# Normal use
print(compute_discount(100, 20))  # 80.0

# Absurd value: we raise an explicit error
print(compute_discount(100, 150))

The call with 150 raises a ValueError with your message. Whoever uses your function can then catch it with try/except:

try:
    compute_discount(100, 150)
except ValueError as e:
    print(f"Discount refused: {e}")  # Discount refused: Percent must be between 0 and 100.

Note: choose the most meaningful exception type. ValueError for an invalid value, TypeError for a wrong type, or a custom exception (created with class MyError(Exception): pass) for cases truly specific to your application.

assert: checking an assumption

The assert statement checks that a condition is true. If it is false, Python raises an AssertionError with the message you provide. The syntax is: assert condition, "message".

def average(grades):
    assert len(grades) > 0, "The grade list must not be empty."
    return sum(grades) / len(grades)

print(average([12, 15, 18]))  # 15.0

# This assumption is false: assert raises an AssertionError
print(average([]))

It is very handy to check an assumption during development, to debug, or in tests (assert result == 42).

Warning: assert statements can be disabled when Python runs in optimized mode (python -O): they are then simply ignored. So never use them to validate user input in production. For that, use a real if followed by a raise.

Recap

  • An exception interrupts the program; without protection, it stops.
  • try wraps the risky code, except TypeError catches a specific type.
  • Group several types with except (KeyError, TypeError) and grab the message with as e.
  • Never use a bare except:: it hides real bugs.
  • else runs if all went well; finally always runs (cleanup).
  • raise raises your own exception to signal an anomaly in your code.
Avec l'IA

Copiez ce prompt dans Claude ou ChatGPT :

Écris une fonction Python qui demande un âge à l'utilisateur et le valide. Elle doit gérer le cas où ce n'est pas un nombre (ValueError) et lever une erreur si l'âge est négatif ou supérieur à 130. Utilise try/except et raise.
Ré-explique sans regarder

Sans relire la réponse de l'IA : dans cette fonction de validation d'âge, à quoi sert le try/except et à quoi sert le raise ? Pourquoi deux mécanismes différents ?

Le try/except réagit à une erreur déjà levée par Python : int(saisie) peut lever une ValueError si l'utilisateur tape abc, on l'attrape pour ne pas planter. Le raise, lui, déclenche volontairement une erreur quand une règle métier est violée (âge négatif ou > 130) : la valeur est un entier valide pour Python, mais absurde pour toi. En clair : except = je subis une erreur et je la gère ; raise = je signale moi-même une situation anormale.
Accepter ou rejeter le code de l'IA

L'IA te propose ce code pour lire un fichier de configuration. Ton rôle de relecteur : l'accepter tel quel ou le rejeter, et dire pourquoi.

def lire_config(chemin):
    try:
        with open(chemin) as f:
            return f.read()
    except:
        return None
À rejeter. Le except: nu attrape tout : un fichier introuvable (FileNotFoundError), mais aussi une faute de frappe dans ton code, un KeyboardInterrupt, une MemoryError… et renvoie silencieusement None dans tous les cas. Résultat : le jour où la config est mal lue à cause d'un vrai bug, tu cherches pendant des heures car l'erreur a été avalée. Corrige en ciblant le type attendu : except FileNotFoundError: (et laisse remonter le reste). C'est exactement le piège du except nu vu plus haut.
Rappel libre

Sans remonter dans la leçon : que se passe-t-il si une exception est levée dans un try mais qu'aucun except ne correspond à son type ? Et quand s'exécute le bloc finally ?

Si aucun except ne correspond au type de l'exception, elle n'est pas attrapée : elle continue de remonter (le try ne la bloque pas), et si rien ne l'attrape plus haut, le programme s'arrête sur une traceback. Le finally, lui, s'exécute toujours : que le try réussisse, qu'un except attrape l'erreur, ou même que l'exception reparte vers le haut. C'est pour ça qu'on y met le nettoyage (fermer un fichier, une connexion).
Quel bloc s'exécute toujours, qu'il y ait eu une erreur ou non ?
Pourquoi éviter un except: nu (sans type) ?
Que fait le mot-clé raise ?
Prochaine étape

Votre code sait maintenant encaisser l'imprévu sans planter. Parfait, car la prochaine étape réserve son lot de surprises : aller chercher des données sur le web. On interroge des API REST avec requests et on extrait le contenu de pages avec BeautifulSoup, là où les erreurs réseau sont la norme.

Leçon 9 : API et web scraping →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement