Python est bien connu par sa flexibilité qui vient du fait d'adopter et de promouvoir le duck typing. Pour écrire du code vraiment pythonic, on ne se préoccupe pas vraiment des types des objets qu'on manipule, mais des méthodes et attributs dont il dispose et de leur comportement. Il faut voir le type checking comme une sorte de test low cost. En effet, au lieu d'écrire des scénarios de test, on annote certaines parties du code avec des types. Et au lieu d'exécuter le code, on utilise un moteur qui vérifie si les types sont toujours utilisés de manière homogène.
Dans cet article nous allons parler du support au type checking en Python. On montrera avec un exemple simple comment annoter du code avec des types et on utilisera mypy, un moteur de vérification de type statique pour vérifier le code de notre exemple.
Un exemple simple
Supposons qu'on écrit un module qui expose une fonction add_printer qui manipule un dictionnaire qui associe des strings à des fonctions qui prennent comme entrée une valeur et retournent une string générée à partir de la valeur passée.
En temps d'exécution, on ne peut pas obliger les utilisateurs d'utiliser la fonction comme attendu. On ne peut pas non plus les obliger à respecter le contrat avec les utilisateurs suite à des changements de codes futurs.
Par exemple, on ne peut pas empêcher un utilisateur d'utiliser le module ainsi :
On aura donc potentiellement une erreur pendant l'exécution du programme. C'est à dire... trop tard.
Premier pas : on annote notre module
On commence par annoter le module avec les types attendus et produits :
Trois choses à savoir pour comprendre cet exemple :
La syntaxe nomvariable: type = ... est utilisée pour typer une variable, par exemple name: str veut dire que name a le type string ;
Les types primitifs Python sont représentés par des symboles tels que str et int ;
Des types complexes faisant partie du langage doivent importés du module typing, par exemple Dict[str, Printer] représente le type dictionnaire où les clefs sont des strings et les valeurs sont des objets du type Printer ;
La syntaxe A = B sert à définir un type alias, i.e. Dans cet exemple, un type A est équivalent au type B. Un autre exemple serait Printer = Callable[[int], str] qui déclare le type Printer comme étant une fonction qui prend en entrée un entier et qui renvoie une string.
Les annotations servent à documenter les entrées et sorties des fonctions et objets définis dans notre module. Pour que ces annotations soient effectives, il faudra un support automatique qui vérifie que les types sont utilisés de façon cohérente tout au long du programme.
Ensuite on vérifie nos annotations
Dans cet article, on utilisera mypy pour vérifier automatiquement l'utilisation des annotations dans notre programme.
Pour installer mypy, on utilisera pip:
Pour lancer la vérification de types sur un fichier on fait :
Le moteur va donc lister tous les erreurs de type dans l'utilisation de la librairie. Par exemple :
mypy est capable de détecter que la fonction a été déclarée avec le mauvais type de sortie sans pour autant exécuter le code.
Les erreurs de type n'empêchent pas Python d'exécuter le code de l'application, ce qui permet une adoption progressive des types et leur vérification dans une vraie application. On n'est pas non plus obligé d'annoter toutes les variables et fonctions. Mais, bien évidemment, cela réduira le nombre d'erreurs que l'on pourra détecter avec n'importe quel outil de vérification automatique.
Pour aller plus loin
J'espère que cet article vous a donné envie d'expérimenter avec les annotations et les moteurs de vérification de types en Python. Le sujet est très vaste et en même temps pas encore très exploré dans la communauté Python.
Dans cette section, je vous donne quelques liens utiles pour approfondir le sujet.
Quelques articles
Voici quelques tutoriels un peu plus détaillés que cet article :
Quelques outils
Vous pouvez aussi jouer avec quelques outils qui utilisent ou génèrent les annotations Python :
Obiwan a runtime type checker (contract system) and JSON validator.
Typecheck decorator a decorator for functions (...), the decorator will perform dynamic argument type checking for every call to the function.
Ensure is a set of simple assertion helpers that let you write more expressive, literate, concise, and readable Pythonic code for validating conditions. (...) If you use Python 3, you can use ensure to enforce your function signature annotations.
MonkeyType génère des types statiques en fonction des résultats de monitoring runtime.
Un peu d'histoire...
Pour finir, pour ceux qui aiment lire des PEPs, il y en a quelques unes qui racontent l'histoire, les détails d'implémentation et les trade-offs de la vérification syntaxique en Python. Je vous recommande de les lire dans l'ordre de publication :
PEP 3107 : Une première PEP, publiée en 2006, codifie la syntaxe à utiliser pour annoter les paramètres des fonctions. Cette spec ne donne pas une sémantique, le but étant de laisser à des outils tiers la charge de leur donner une sémantique appropriée.
PEP 482 : Publiée en janvier 2015, prépare les PEPs qui définissent la syntaxe et la sémantique du type checking en Python qui sera précisée dans les PEPs suivantes.
PEP 483 : Restée en draft, cette PEP contient une discussion théorique sur les types décrits dans la PEP 484.
PEP 484 : Publiée en mai 2015, est la base de tout le travail en syntax checking en Python.
PEP 526 : Publiée en 2016, étends la PEP 484 avec des annotations pour les variables.
PEP 544 : Créée en 2017, mais encore un draft, étends la PEP 484 avec le static duck typing, i.e. fait en sorte que les moteurs d'inférence puissent inférer des types complexes sans l'aide du développeur.