# 8. Erreurs et exceptions

Jusqu'ici, les messages d'erreurs ont seulement été mentionnés. Mais si vous avez essayé les exemples vous avez certainement vu plus que cela. En fait, il y a au moins deux types d'erreurs à distinguer : les erreurs de syntaxe et les exceptions.

## 8.1. Les erreurs de syntaxe

Les erreurs de syntaxe, qui sont des erreurs d'analyse du code, sont peut-être celles que vous rencontrez le plus souvent lorsque vous êtes encore en phase d'apprentissage de Python :

    >>> while True print('Hello world')
      File "<stdin>", line 1
        while True print('Hello world')
                       ^
    SyntaxError: invalid syntax

L'analyseur indique la ligne incriminée et affiche une petite « flèche » pointant vers le premier endroit de la ligne où l'erreur a été détectée. L'erreur est causée (ou, au moins, a été détectée comme telle) par le symbole placé avant la flèche. Dans cet exemple la flèche est sur la fonction `print()` car il manque deux points (`':'`) juste avant. Le nom du fichier et le numéro de ligne sont affichés pour vous permettre de localiser facilement l'erreur lorsque le code provient d'un script.

## 8.2. Exceptions

Même si une instruction ou une expression est syntaxiquement correcte, elle peut générer une erreur lors de son exécution. Les erreurs détectées durant l'exécution sont appelées des exceptions et ne sont pas toujours fatales : nous apprendrons bientôt comment les traiter dans vos programmes. La plupart des exceptions toutefois ne sont pas prises en charge par les programmes, ce qui génère des messages d'erreurs comme celui-ci :

    >>> 10 * (1/0)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ZeroDivisionError: division by zero
    >>> 4 + spam*3
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    NameError: name 'spam' is not defined
    >>> '2' + 2
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: can only concatenate str (not "int") to str

La dernière ligne du message d'erreur indique ce qui s'est passé. Les exceptions peuvent être de différents types et ce type est indiqué dans le message : les types indiqués dans l'exemple sont `ZeroDivisionError`, `NameError` et `TypeError`. Le texte affiché comme type de l'exception est le nom de l'exception native qui a été déclenchée. Ceci est vrai pour toutes les exceptions natives mais n'est pas une obligation pour les exceptions définies par l'utilisateur (même si c'est une convention bien pratique). Les noms des exceptions standards sont des identifiants natifs (pas des mots-clef réservés).

Le reste de la ligne fournit plus de détails en fonction du type de l'exception et de ce qui l'a causée.

La partie précédente du message d'erreur indique le contexte dans lequel s'est produite l'exception, sous la forme d'une trace de pile d'exécution. En général, celle-ci contient les lignes du code source ; toutefois, les lignes lues à partir de l'entrée standard ne sont pas affichées.

Vous trouvez la liste des exceptions natives et leur signification dans Exceptions natives.

## 8.3. Gestion des exceptions

Il est possible d'écrire des programmes qui prennent en charge certaines exceptions. Regardez l'exemple suivant, qui demande une saisie à l'utilisateur jusqu'à ce qu'un entier valide ait été entré, mais permet à l'utilisateur d'interrompre le programme (en utilisant ``Control`-`C`` ou un autre raccourci que le système accepte) ; notez qu'une interruption générée par l'utilisateur est signalée en levant l'exception `KeyboardInterrupt`. :

    >>> while True:
    ...     try:
    ...         x = int(input("Please enter a number: "))
    ...         break
    ...     except ValueError:
    ...         print("Oops!  That was no valid number.  Try again...")
    ...

L'instruction `try` fonctionne comme ceci :

- premièrement, la clause try (instruction(s) placée(s) entre les mots-clés `try` et `except`) est exécutée.

- si aucune exception n'intervient, la clause `except` est sautée et l'exécution de l'instruction `try` est terminée.

- si une exception intervient pendant l'exécution de la clause `try`, le reste de cette clause est sauté. Si le type d'exception levée correspond à un nom indiqué après le mot-clé `except`, la clause `except` correspondante est exécutée, puis l'exécution continue après le bloc `try`/`except`.

- si une exception intervient et ne correspond à aucune exception mentionnée dans la clause `except`, elle est transmise à l'instruction `try` de niveau supérieur ; si aucun gestionnaire d'exception n'est trouvé, il s'agit d'une exception non gérée et l'exécution s'arrête avec un message comme indiqué ci-dessus.

Une instruction `try` peut comporter plusieurs clauses `except` pour permettre la prise en charge de différentes exceptions. Mais un seul gestionnaire, au plus, sera exécuté. Les gestionnaires ne prennent en charge que les exceptions qui interviennent dans la clause `try` correspondante, pas dans d'autres gestionnaires de la même instruction `try`. Mais une même clause `except` peut citer plusieurs exceptions sous la forme d'un n-uplet entre parenthèses, comme dans cet exemple :

    ... except (RuntimeError, TypeError, NameError):
    ...     pass

Une classe dans une clause `except` est compatible avec une exception si elle est de la même classe ou d'une de ses classes dérivées. Mais l'inverse n'est pas vrai, une clause `except` spécifiant une classe dérivée n'est pas compatible avec une classe de base. Par exemple, le code suivant affiche B, C et D dans cet ordre :

    class B(Exception):
        pass

    class C(B):
        pass

    class D(C):
        pass

    for cls in [B, C, D]:
        try:
            raise cls()
        except D:
            print("D")
        except C:
            print("C")
        except B:
            print("B")

Notez que si les clauses `except` avaient été inversées (avec `except B` en premier), il aurait affiché B, B, B — la première clause `except` qui correspond est déclenchée.

Toutes les exceptions héritent de `BaseException`, il est donc possible de s'en servir comme un joker. C'est toutefois à utiliser avec beaucoup de précautions car il est facile de masquer une vraie erreur de programmation par ce biais. Elle peut aussi être utilisée pour afficher un message d'erreur avant de propager l'exception (en permettant à un appelant de gérer également l'exception) :

    import sys

    try:
        f = open('myfile.txt')
        s = f.readline()
        i = int(s.strip())
    except OSError as err:
        print("OS error: {0}".format(err))
    except ValueError:
        print("Could not convert data to an integer.")
    except BaseException as err:
        print(f"Unexpected {err=}, {type(err)=}")
        raise

Concernant la dernière clause `except`, vous pouvez omettre les noms des exceptions, cependant la valeur de l'exception doit alors être récupérée avec `sys.exc_info()[1]`.

L'instruction `try` … `except` accepte également une clause `else` optionnelle qui, lorsqu'elle est présente, doit se placer après toutes les clauses `except`. Elle est utile pour du code qui doit être exécuté lorsqu'aucune exception n'a été levée par la clause `try`. Par exemple :

    for arg in sys.argv[1:]:
        try:
            f = open(arg, 'r')
        except OSError:
            print('cannot open', arg)
        else:
            print(arg, 'has', len(f.readlines()), 'lines')
            f.close()

Il vaut mieux utiliser la clause `else` plutôt que d'ajouter du code à la clause `try` car cela évite de capturer accidentellement une exception qui n'a pas été levée par le code initialement protégé par l'instruction `try` … `except`.

Quand une exception intervient, une valeur peut lui être associée, que l'on appelle l'argument de l'exception. La présence de cet argument et son type dépendent du type de l'exception.

La clause `except` peut spécifier un nom de variable après le nom de l'exception. Cette variable est liée à une instance d'exception avec les arguments stockés dans `instance.args`. Pour plus de commodité, l'instance de l'exception définit la méthode `__str__()` afin que les arguments puissent être affichés directement sans avoir à référencer `.args`. Il est possible de construire une exception, y ajouter ses attributs, puis la lever plus tard.

    >>> try:
    ...     raise Exception('spam', 'eggs')
    ... except Exception as inst:
    ...     print(type(inst))    # the exception instance
    ...     print(inst.args)     # arguments stored in .args
    ...     print(inst)          # __str__ allows args to be printed directly,
    ...                          # but may be overridden in exception subclasses
    ...     x, y = inst.args     # unpack args
    ...     print('x =', x)
    ...     print('y =', y)
    ...
    <class 'Exception'>
    ('spam', 'eggs')
    ('spam', 'eggs')
    x = spam
    y = eggs

Si une exception a un argument, il est affiché dans la dernière partie du message des exceptions non gérées.

Les gestionnaires d'exceptions n'interceptent pas que les exceptions qui sont levées immédiatement dans leur clause `try`, mais aussi celles qui sont levées au sein de fonctions appelées (parfois indirectement) dans la clause `try`. Par exemple :

    >>> def this_fails():
    ...     x = 1/0
    ...
    >>> try:
    ...     this_fails()
    ... except ZeroDivisionError as err:
    ...     print('Handling run-time error:', err)
    ...
    Handling run-time error: division by zero

## 8.4. Déclencher des exceptions

L'instruction `raise` permet au programmeur de déclencher une exception spécifique. Par exemple :

    >>> raise NameError('HiThere')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    NameError: HiThere

Le seul argument à `raise` indique l'exception à déclencher. Cela peut être soit une instance d'exception, soit une classe d'exception (une classe dérivée de `Exception`). Si une classe est donnée, elle est implicitement instanciée via l'appel de son constructeur, sans argument :

    raise ValueError  # shorthand for 'raise ValueError()'

Si vous avez besoin de savoir si une exception a été levée mais que vous n'avez pas intention de la gérer, une forme plus simple de l'instruction `raise` permet de propager l'exception :

    >>> try:
    ...     raise NameError('HiThere')
    ... except NameError:
    ...     print('An exception flew by!')
    ...     raise
    ...
    An exception flew by!
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
    NameError: HiThere

## 8.5. Chaînage d'exceptions

Si une exception non gérée se produit à l'intérieur d'une section `except`, l'exception en cours de traitement est jointe à l'exception non gérée et incluse dans le message d'erreur :

    >>> try:
    ...     open("database.sqlite")
    ... except OSError:
    ...     raise RuntimeError("unable to handle error")
    ...
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
    FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
      File "<stdin>", line 4, in <module>
    RuntimeError: unable to handle error

Pour indiquer qu'une exception est la conséquence directe d'une autre, l'instruction `raise` autorise une clause facultative `from` :

    # exc must be exception instance or None.
    raise RuntimeError from exc

Cela peut être utile lorsque vous transformez des exceptions. Par exemple :

    >>> def func():
    ...     raise ConnectionError
    ...
    >>> try:
    ...     func()
    ... except ConnectionError as exc:
    ...     raise RuntimeError('Failed to open database') from exc
    ...
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
      File "<stdin>", line 2, in func
    ConnectionError

    The above exception was the direct cause of the following exception:

    Traceback (most recent call last):
      File "<stdin>", line 4, in <module>
    RuntimeError: Failed to open database

Cela permet également de désactiver le chaînage automatique des exceptions à l'aide de l'idiome `from None` :

    >>> try:
    ...     open('database.sqlite')
    ... except OSError:
    ...     raise RuntimeError from None
    ...
    Traceback (most recent call last):
      File "<stdin>", line 4, in <module>
    RuntimeError

Pour plus d'informations sur les mécanismes de chaînage, voir Exceptions natives.

## 8.6. Exceptions définies par l'utilisateur

Les programmes peuvent nommer leurs propres exceptions en créant une nouvelle classe d'exception (voir Classes pour en savoir plus sur les classes de Python). Les exceptions sont typiquement dérivées de la classe `Exception`, directement ou non.

Les classes d'exceptions sont des classes comme les autres, et peuvent donc utiliser toutes les fonctionnalités des classes. Néanmoins, en général, elles demeurent assez simples, et se contentent d'offrir des attributs qui permettent aux gestionnaires de ces exceptions d'extraire les informations relatives à l'erreur qui s'est produite.

La plupart des exceptions sont définies avec des noms qui se terminent par "Error", comme les exceptions standards.

Beaucoup de modules standards définissent leurs propres exceptions pour signaler les erreurs possibles dans les fonctions qu'ils définissent. Plus d'informations sur les classes sont présentées dans le chapitre Classes.

## 8.7. Définition d'actions de nettoyage

L'instruction `try` a une autre clause optionnelle qui est destinée à définir des actions de nettoyage devant être exécutées dans certaines circonstances. Par exemple :

    >>> try:
    ...     raise KeyboardInterrupt
    ... finally:
    ...     print('Goodbye, world!')
    ...
    Goodbye, world!
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
    KeyboardInterrupt

Si la clause `finally` est présente, la clause `finally` est la dernière tâche exécutée avant la fin du bloc `try`. La clause `finally` se lance que le bloc `try` produise une exception ou non. Les prochains points parlent de cas plus complexes lorsqu'une exception apparait :

- Si une exception se produit durant l'exécution de la clause `try`, elle peut être récupérée par une clause `except`. Si l'exception n'est pas récupérée par une clause `except`, l'exception est levée à nouveau après que la clause `finally` a été exécutée.

- Une exception peut se produire durant l'exécution d'une clause `except` ou `else`. Encore une fois, l'exception est reprise après que la clause `finally` a été exécutée.

- Si dans l'exécution d'un bloc `finally`, on atteint une instruction `break`, `continue` ou `return`, alors les exceptions ne sont pas reprises.

- Si dans l'exécution d'un bloc `try`, on atteint une instruction `break`, `continue` ou `return`, alors la clause `finally` s'exécute juste avant l'exécution de `break`, `continue` ou `return`.

- Si la clause `finally` contient une instruction `return`, la valeur retournée sera celle du `return` de la clause `finally`, et non la valeur du `return` de la clause `try`.

Par exemple :

    >>> def bool_return():
    ...     try:
    ...         return True
    ...     finally:
    ...         return False
    ...
    >>> bool_return()
    False

Un exemple plus compliqué :

    >>> def divide(x, y):
    ...     try:
    ...         result = x / y
    ...     except ZeroDivisionError:
    ...         print("division by zero!")
    ...     else:
    ...         print("result is", result)
    ...     finally:
    ...         print("executing finally clause")
    ...
    >>> divide(2, 1)
    result is 2.0
    executing finally clause
    >>> divide(2, 0)
    division by zero!
    executing finally clause
    >>> divide("2", "1")
    executing finally clause
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 3, in divide
    TypeError: unsupported operand type(s) for /: 'str' and 'str'

Comme vous pouvez le voir, la clause `finally` est exécutée dans tous les cas. L'exception de type `TypeError`, déclenchée en divisant deux chaînes de caractères, n'est pas prise en charge par la clause `except` et est donc propagée après que la clause `finally` a été exécutée.

Dans les vraies applications, la clause `finally` est notamment utile pour libérer des ressources externes (telles que des fichiers ou des connexions réseau), quelle qu'ait été l'utilisation de ces ressources.

## 8.8. Actions de nettoyage prédéfinies

Certains objets définissent des actions de nettoyage standards qui doivent être exécutées lorsque l'objet n'est plus nécessaire, indépendamment du fait que l'opération ayant utilisé l'objet ait réussi ou non. Regardez l'exemple suivant, qui tente d'ouvrir un fichier et d'afficher son contenu à l'écran :

    for line in open("myfile.txt"):
        print(line, end="")

Le problème avec ce code est qu'il laisse le fichier ouvert pendant une durée indéterminée après que le code a fini de s'exécuter. Ce n'est pas un problème avec des scripts simples, mais peut l'être au sein d'applications plus conséquentes. L'instruction `with` permet d'utiliser certains objets comme des fichiers d'une façon qui assure qu'ils seront toujours nettoyés rapidement et correctement.

    with open("myfile.txt") as f:
        for line in f:
            print(line, end="")

Après l'exécution du bloc, le fichier f est toujours fermé, même si un problème est survenu pendant l'exécution de ces lignes. D'autres objets qui, comme pour les fichiers, fournissent des actions de nettoyage prédéfinies l'indiquent dans leur documentation.