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.
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 variable | compteur: int = 0 |
| Un paramètre | def envoyer(destinataire: str) |
| Un retour | -> bool |
| Une fonction qui ne renvoie rien | -> None |
| Une collection | list[str] ou dict[str, int] |
| Une valeur parfois absente | str | 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.
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.
def chercher_client(identifiant: int) -> dict:
ligne = base.lire(identifiant)
if ligne is None:
return None # la signature promettait un dictionnaire
return ligneLe 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
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.
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.
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.