Python est sans doute le langage de programmation le plus en vogue depuis quelques années. Étant donné son excellente conception algorithmique, il est très répandu dans l’univers de l’analyse de données et du machine-learning. En comparaison avec d’autres langages, Python s’appuie sur une syntaxe intuitive qui encapsule des concepts d’implémentation de bas niveau ce qui rend le code agréable à lire et facilement compréhensible par l’œil humain.

Cette simplicité a généré un fort intérêt des développeurs. Ainsi, il connait un essor important dans le développement d’applications web. L’un des points positifs du langage est le gain en productivité pendant le développement d’applications web par rapport à d’autres langages à typage statique comme Java ou C++. Par conséquent, un gain de temps précieux pour les entreprises qui font de plus en plus le choix de Python.

Paradoxalement, la simplicité de Python peut devenir problématique. Les applications sont plus rapidement en production mais elles peuvent également contenir plus de bugs. L’une des critiques souvent évoquée sur Python est son typage dynamique. En effet, le type de variables est assigné lors de la déclaration et il peut être modifié pendant l’interprétation du code.

Pour palier cette problématique, Python 3.5 a introduit le « type hinting » (PEP484: Type Hints). Dans cet article on expliquera comment fonctionne le typage en python, quelles stratégies adopter afin d’exploiter le type hinting dans une base de code et comment vérifier les types pendant l’exécution d’un programme. Les examples dans cet article sont compatibles avec python 3.8.

Bugs ain't fun.

Comment typer en python ?

Le typage se fait grâce aux annotations. Elles permettent d’associer un type donné (List, bool, etc) aux arguments et aux retours des fonctions.

MyPy, static type checker : Cet outil met en évidence les incohérences dans le code en vérifiant les annotations de type.

Ces annotations sont des indices pour le static type checker de MyPy. Ainsi, le type hinting ne modifie pas le fonctionnement pendant l’exécution Python. En revanche, Il donne de la visibilité sur les potentiels bugs pendant le développement.

MyPy Built-in type :

from typing import List, Dict, Tuple, Dict, Iterable, Sequence, Mapping, Union, Any

Les types primitifs sont les plus récurrents et utilisés : bool, int, str, float. Il peuvent être utilisés pour typer les arguments ainsi que les retours des fonctions. Dans l’exemple suivant, la fonction circle_surface prend en argument le rayon du cercle (le radius) et calcule la surface de ce cercle. Cet argument est de type float (indiqué après le : suivant le nom de l’argument) et la réponse est elle aussi de type float (le type de retour est indiqué après la flèche ->).

def circle_surface(radius: float) -> float:
    return 3.141516 * math.sqrt(radius) 

Il est également possible de créer des types composés comme des listes d’entiers (integer) ou comme dans cet example des listes de nombres flottants (floats).

from typing import List

Vector = List[float]

def scale(scalar: float, vector: Vector) -> Vector:
    return [scalar * num for num in vector]

Le typage peut se faire également sur toutes les classes définies dans un programme y compris celles des librairies externes.

# l'utilisation des type alias rend la lecture
# plus claire et la maintenance du code plus simple. 
MongoPipeline = List[Mapping[str, Any]]
MongoQuery = Union[Mapping[str, Any], MongoPipeline]

def get_entity(mongo_query: MongoQuery) -> Optional[Entity]:
    if result := MongoClient().collection.find(mongo_query)
        return Entity(**result)

Bien que ce soit possible, il est parfois risqué de typer des arguments qui proviennent de librairies externes si elles n’utilisent pas le typage. Le contrat qu’un argument doit respecter est implicite. Ce sont donc les mainteneurs des librairies externes qui doivent définir les types de manière explicite. D’autre part, les types des librairies peuvent évoluer avec le temps.

# mongo < 4.4
MongoQuery = Mapping[str, Any]

# mongo == 4.4
MongoPipeline = List[Mapping[str, Any]]
MongoQuery = Union[Mapping[str, Any], MongoPipeline]

Ainsi l’utilisation de types alias dans l’exemple ci-dessus n’est pas forcement une bonne idée. Il est cependant important d’évoquer l’existence des alias qui peuvent être utilisés dans son propre code.

L’utilisation des ABC (Abstract Base Classes) est à privilégier aux types concrets. Ainsi, Iterable permet de typer une fonction qui prend différents types en argument tels que tuple, list ou set. Plus restrictif, le type concret list n’accepte que des listes.

Les retours implicites ainsi que les fonctions sans retour sont également pris en compte par MyPy.

def print_variable(s: str) -> None:
    print(s)

def not_ready(s: str) -> NoReturn:
    print('not implemented')
    raise 

Mais comment utiliser concrètement le type hinting pour debugger du code ?

Deux stratégies simples :

  1. Appliquer les type hinting sur les parties du code (classes, fonctions, variables d’itérations, etc) où il y a des erreurs de type. MyPy donnera de la visibilité sur les incohérences de types et cela vous permettra de mieux comprendre d’où vient le problème.

  2. Utiliser les fonctions reveal_type(exp) et/ou reveal_locals. Elles produisent un rapport sur les expressions en paramètre ou sur les variables dans le scope.

Python propose davantage de types tels que les typing.Optional, typing.TypedDict et @dataclass.

Typeguard: vérification du type à l’exécution du programme

Avec la librairie Typeguard on peut exécuter une application dans un serveur de test et/ou local afin de chercher les incohérences de typing dans le code via un IHM ou un script. Ainsi, il est possible de faire de tests avec des données réelles.

Il existent trois manières différentes de tester les types avec Typeguard :

  • check_argument_types() et check_return_type() : ces fonctions doivent être ajoutées dans les fonctions avec un assert.
  • le décorateur @typechecked
  • install_import_hook() : ajoute le décorateur @typechecked à toute fonction typée.

The Zen of Python

La PEP20 (The Zen of Python) décrit en quelques postulats la philosophie du langage. Python est conçu sur des principes qui prônent, entre autres, la beauté, la simplicité, la lisibilité et la flexibilité. Personnellement, je trouve que ces aspects ont été respectés au cours des différentes versions. Le passage de Python 2 à Python 3 d’une application comme Deepki Ready a été plus simple et transparent que certaines mises à jour de librairies.

L’introduction du type hinting dans l’univers Python a été, à mon avis, une bonne manière de palier les problématiques des bugs insidieux qui passent entre les mailles du filet malgré la présence de tests unitaires. De la même manière, le choix laissé au dévelopeur sur la stratégie d’implémentation et le dégré de restrictivité se conjugue avec la philosophie Python.

Le type hinting est un sujet toujours présent dans les dernières versions de Python. La nouveauté en Python 3.9 est l’utilisation des built-in collections comme des types génériques. Ainsi, on n’a plus besoin d’importer les types en majuscules de typing (from typing import List, Dict). N’hésitez pas à consulter la PEP585: Type Hinting Generics In Standard Collections

def greet_all(names: list[str]) -> None:
    for name in names:
        print("Hello", name)

On peut donc conclure que le sujet sera au cœur de l’évolution du langage. Ainsi la critique, souvent évoquée par la communauté de développeurs, concernant le typage et les problèmes sous-jacents ne devrait plus être d’actualité prochainement.