Les annotations de type en Python : documenter ce qu'une fonction attend

Une annotation de type dit ce qu'une fonction attend et ce qu'elle renvoie, pour qu'on l'appelle sans lire son corps. Python, lui, ne vérifie rien.
5 min de lecture
Believemy logo

Définition

Vous tombez sur cet appel dans un fichier que vous n'avez pas écrit : prix_ttc(montant, 0.2). Est-ce que montant est un nombre, une chaîne venue d'un formulaire, un objet commande ? Le nom ne le dit pas : pour le savoir, il faut ouvrir la fonction et lire son corps. L'annotation de type existe pour couper court à cette enquête.

Elle indique quel genre de valeur un nom est censé contenir. Elle s'écrit après deux-points pour une variable ou un paramètre, et après une flèche pour ce qu'une fonction renvoie. Voici la même fonction, annotée.

PYTHON
def prix_ttc(prix_ht: float, taux: float = 0.2) -> float:
    return round(prix_ht * (1 + taux), 2)

La signature répond désormais seule : deux nombres décimaux entrent, un décimal sort.

Reste un point qui surprend tout le monde : Python lit cette ligne, la range de côté, et s'arrête là. Appeler la fonction avec une chaîne ne déclenche aucune alerte, jusqu'à ce que la multiplication échoue bien plus loin avec une TypeError. Une annotation documente une intention, elle ne l'impose jamais.


Ce que Python en fait, et ce qu'il n'en fait pas

Si l'interpréteur ne vérifie rien, à quoi servent-elles ? Python range les annotations dans un dictionnaire attaché à l'objet, __annotations__, et s'en tient là. Tout le bénéfice vient de programmes extérieurs qui le lisent.

Le premier est le vérificateur de types. mypy relit le projet entier sans l'exécuter et signale les types qui ne collent pas : un retour parfois vide traité comme s'il ne l'était jamais, deux arguments dans le mauvais ordre. L'erreur apparaît dans votre terminal, avant le premier lancement, plutôt que chez un utilisateur.

Le second est votre éditeur : des paramètres annotés, et la complétion propose les bonnes méthodes pendant que vous écrivez.

Quelques bibliothèques, enfin, les lisent vraiment à l'exécution. Une dataclass construit ses champs à partir des annotations de la classe, et Pydantic s'en sert pour refuser les données qui ne collent pas. L'annotation devient du code qui agit : s'y tromper ne coûte plus une lecture confuse, mais un bug.


Les formes que vous croiserez

Six écritures reviennent sans cesse, et la première semaine suffit à les croiser toutes.

Ce que vous annotezÉcriture
Une variablecompteur: int = 0
Un paramètredef envoyer(destinataire: str)
Un retour-> bool
Une fonction qui ne renvoie rien-> None
Une collectionlist[str] ou dict[str, int]
Une valeur parfois absentestr | None

Le contenu des collections mérite d'être précisé : c'est là que l'annotation cesse d'être décorative. Une list toute seule dit qu'il y a une liste ; list[str] dit ce qu'on trouvera dedans, donc ce qu'on a le droit d'en faire. Même logique pour un dict, clés et valeurs séparément.

Attention

Ces écritures dépendent de la version : list[str] sans import demande Python 3.9, str | None demande la 3.10. Sur un serveur resté en 3.8, le fichier refuse simplement de s'importer : les équivalents List[str] et Optional[str] du module typing existent pour ces cas.


Le piège : l'annotation qui ment

Puisque rien ne vérifie les annotations, rien ne les empêche de vieillir. Le scénario est toujours le même : la fonction est annotée juste, puis un cas de retour s'ajoute six mois plus tard sans que personne ne corrige la signature.

PYTHON
def chercher_client(identifiant: int) -> dict:
    ligne = base.lire(identifiant)
    if ligne is None:
        return None        # la signature promettait un dictionnaire
    return ligne

Le code tourne et personne ne proteste. La casse se produit ailleurs : l'appelant a lu la signature, en a conclu qu'il recevrait un dictionnaire, et écrit un accès par clé. Le jour où le client n'existe pas, l'erreur remonte chez lui, loin de la fonction fautive.

L'annotation exacte serait dict | None : elle rend le None visible et oblige l'appelant à prévoir le cas vide. Fausses, les annotations sont pires qu'absentes : on les lit comme une garantie.


Faut-il en mettre partout ?

Non, l'usage s'est stabilisé sur un partage simple : les frontières y gagnent, l'intérieur des fonctions non. Une frontière, c'est tout endroit appelé sans être lu, à commencer par ce qu'un module expose au reste du projet. Une variable de boucle qui vit trois lignes n'apprend rien à personne.

Dernier malentendu : les annotations n'annulent pas le duck typing qui fait la souplesse de Python, elles le décrivent. Annoter un paramètre par un protocole plutôt que par une classe revient à écrire « tout ce qui sait faire ceci ».


Questions fréquentes

Question

Les annotations ralentissent-elles le programme ?

De façon négligeable : elles sont évaluées une fois, au chargement du module, puis rangées dans un dictionnaire que rien ne relit. Si ce coût compte, la PEP 563 permet de les traiter comme du texte et d'en repousser l'évaluation.

Question

Que faire quand le type est trop compliqué à écrire ?

Commencez large, resserrez ensuite : list vaut mieux que rien, et list[dict] vaut mieux que list. Si l'écriture devient illisible, c'est la structure de données qui mérite une classe dédiée, pas l'annotation qui mérite une écriture plus savante.

Question

Une annotation remplace-t-elle la docstring ?

Non, elles ne disent pas la même chose : l'annotation donne la forme de la valeur, la docstring son sens, son unité et ses conditions d'appel. Un retour annoncé en nombre décimal ne dira jamais s'il s'agit d'euros ou de secondes. La formation Python montre les deux à l'œuvre sur le même code.

Termes connexes

Découvrez notre glossaire Python

Parcourez les termes et définitions les plus couramment utilisés dans le domaine du développement avec Python.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.