[JOUR 18] II. Python, the right way : Types, Type Hints et Mypy
Le typage, la base pour un code professionel
Sommaire :
I. Introduction
II. Mypy
III. Les types utilisables avec Mypy
Article précédent :
I. Introduction
Le type hinting en Python consiste simplement à préciser les types dans le code.
Grâce à cela, nous pouvons non seulement anticiper les erreurs de types, mais aussi permettre à notre IDE de nous faire des suggestions intelligentes en fonction du type.
Voici un exemple simple avec et sans type hinting dans VS Code :
Au début, la variable nom
n'est pas indiquée comme une chaîne de caractères (string), donc si j'utilise Ctrl + Espace pour obtenir des suggestions, VS Code ne me retourne rien.
Ensuite, je type la variable en str
pour string, et maintenant, lorsque je demande des suggestions, j'obtiens toutes les fonctions qui sont nativement disponibles avec une chaîne de caractères.
Un autre avantage des type hints, en plus de réduire le nombre d’erreurs, est d’augmenter la lisibilité du code. C'est un point non négligeable dans le cadre professionnel où les projets ont souvent plusieurs milliers de lignes de code. Mais voyons sans plus tarder un outil standard pour vérifier le type hinting du code.
II. Mypy
Mypy est un outil d'analyse de code et plus particulièrement des type hints. Il permet de s'assurer que le code est cohérent avec les type hints présents. Si ce n'est pas le cas, il nous retourne des erreurs.
Découvrons les fonctionnalités de Mypy.
Créons un nouveau projet Python avec uv
dans un nouveau dossier :
mkdir type_hint_project
cd type_hint_project
uv init
uv add mypy
Créons un fichier pour tester nos type hints :
cat <<EOF>type_hint_test.py
def salutation(prenom):
return "salut" + prenom
if __name__ == "__main__" :
salutation("Pedro")
EOF
Nous pouvons ensuite installer l’extension VS Code Mypy. Par défaut, l'extension ne fait rien si on ne configure pas Mypy.
En bas à droite dans VS Code, notez que vous avez deux accolades {}
à côté de Python
.
En cliquant dessus, vous verrez ceci :
Pylance fournit en effet des fonctionnalités pour contrôler le type hinting. Vous pouvez les essayer, mais nous ne les utiliserons pas. Nous utiliserons plutôt Mypy car Mypy est agnostique de l’IDE et peut s’utiliser en ligne de commande et dans les outils de CI/CD (continuous integration et continuous delivery, je vous parlerai de ces notions plus tard dans la formation mais retenez juste que ce sont des catégories d’outils qui permettent d’automatiser les tests sur notre code et son déploiement). Ainsi il vaut mieux prendre l’habitude d’utiliser Mypy.
Après installation du plugin Mypy dans VS Code vous aurez plutôt ceci :
En appuyant sur Open logs vous aurez une fenêtre qui s’ouvrira avec les vérifications qu’effectue Mypy sur vos fichiers.
Dans le fichier pyproject.toml
, ajoutez une section :
[tool.mypy]
strict = "True"
Et constatez le résultat sur les fichiers Python dans VS Code après sauvegarde :
Notre fonction et son appel sont surlignés. Si nous passons notre souris dessus, nous pouvons voir différents messages :
“Function is missing a type annotation”
“Call to untyped function “salutation” in typed context” :
En effet, comme nous avons configuré Mypy en mode strict, celui-ci ne laisse passer aucune fonction et appel non typé.
Réparons les problèmes en ajoutant le type du paramètre d’entrée et le type de retour de notre fonction.
Après avoir sauvegardé notre fichier, plus d'erreurs. Super !
Note : Mypy est un outil de vérification de type très configurable. Vous pouvez commencer avec le mode --strict
pour une vérification complète, puis désactiver certaines vérifications selon vos besoins, comme l'option --ignore-missing-imports
pour les bibliothèques tierces sans types définis. Pour apprendre à configurer progressivement le mode strict, vous pouvez consulter la documentation appropriée.
Voici les bases de comment fonctionne Mypy. Sachez que vous pouvez aussi vérifier un fichier ou un projet en entier avec la commande mypy dans votre terminal. Par exemple :
mypy --strict your_script.py # applique mypy en mode strict sur un fichier spécifique
mypy --strict . # applique sur tout le répertoire courant
Maintenant que nous savons détecter les erreurs de types, voyons ensemble les types utilisables dans Mypy.
III. Les types utilisables avec Mypy
Je commence par le type à ne jamais utiliser : le type Any
.
Any
indique à Mypy de ne pas vérifier le type … Autant ne pas utiliser de type hints et Mypy si vous laissez des Any
partout. Vous pouvez voir Any
comme une béquille temporaire pour gérer des problèmes de types, par exemple si vous introduisez Mypy petit à petit dans une codebase, mais sinon évitez-le comme la peste.
Une autre béquille plus sûre est le type object
. Le type object
est un autre type qui peut avoir n’importe quel type comme valeur. Contrairement à Any
, object
est un type statique ordinaire, et seules les opérations valides pour tous les types sont acceptées pour les valeurs de type object
.
Si possible évitez le également. Car une bonne règle de base est de typer avec le type le plus spécifique possible. Et donc il est quasiment impossible que object soit le plus spécifique à votre cas.
Parlons donc plus sérieusement si vous le voulez bien, et parlons de “vrais” types utiles pour le type hinting.
Les types de base
int
: entierfloat
: nombre à virgule flottantebool
: booléen
La plupart du temps, ces types sont utilisés dans les en-têtes des fonctions mais pas forcément ensuite, car ils sont souvent redondants. En effet, Mypy peut inférer le type d’une variable à partir de sa valeur.
Les collections
Liste des différentes collections :
list
: collection d’éléments avec un index. La même valeur peut revenir plusieurs fois.set
: collection d’éléments uniques sans index.dict
: collection de clés valeurs, les clés sont uniques.tuple
: collection d’éléments avec un index comme une liste mais non modifiable après création.str
: chaîne de caractèresbytes
: séquence d’octets
À partir de Python 3.9, les types des collections sont définis entre crochets. Exemple :
mes_entiers : list[int] = [1, 2, 3]
Pour Python 3.8 et avant, les types de collection doivent être importés depuis le module typing
.
Type union et optionnel
À partir de Python 3.10, nous pouvons utiliser |
pour préciser qu’une variable peut être de plusieurs types.
Précédemment, il était nécessaire d’utiliser le type Union
ou Optional
lorsque la variable était soit nulle, soit d’un certain type.
entier_ou_string: int|str = 2 # Union[int, str] avant Python 3.10
liste_entier_ou_string: list[int|str] = [1, "2", 3] # List[Union[int, str]] avant Python 3.10
possible_entier: int|None = None # Optional[int] avant Python 3.10
Les Classes
Pour rappel, en Python, vous pouvez faire de la programmation orientée objet.
Vous pouvez définir des classes et typer des objets grâce à celles-ci.
class Car:
def __init__(self, model: str) -> None:
self.model = model
def get_car_model(car: Car) -> str:
return car.model
À noter que si votre fonction ne retourne pas de valeur, vous devez indiquer None
.
Callable
Les Callables en Python sont tout ce qui est appelable (d’où leurs noms) par des ()
.
Cela regroupe les fonctions basiques, les fonctions lambda, les méthodes, les classes, les instances de classes, les fonctions built-in de Python (len, print, etc.) et les générateurs. Nous parlerons un peu plus des générateurs par la suite.
Nous résumerons les callables au type représentant les fonctions, même si, comme je l'ai écrit, ce type représente un peu plus que ça.
L’un des intérêts d’utiliser le type Callable est de pouvoir assigner une fonction à une variable, à la manière de ce qu’on pourrait avoir dans des langages de programmation fonctionnels.
Ce qui est pratique avec ça, c’est que l’on peut passer des fonctions à des fonctions. Je vous montre un exemple d’utilisation ensuite, mais avant tout, lorsque vous indiquez que votre variable est un callable, vous devez également préciser le type d’entrée et le type de sortie de cette manière :
ma_fonction: Callable[[type_entree], type_sortie]
Un exemple d’utilisation :
from collections.abc import Callable
carre:Callable[[int], int] = lambda x : x ** 2
cube:Callable[[int], int] = lambda x : x ** 3
def apply_function_2_times(f: Callable[[int], int], x : int = 1) -> int :
return f(f(x))
#apply_function_2_times(carre, 2) => resultat : 16
Iterateur et générateur
Itérateur
En Python, on parle d’itérable et d’itérateur. La différence principale entre les deux est qu’un itérable est un objet qui peut être parcouru (comme une liste ou un dictionnaire), tandis qu’un itérateur est l'outil qui permet ce parcours.
Un itérateur est donc un objet qui permet de parcourir une séquence d'éléments en implémentant les méthodes iter() et next().
from __future__ import annotations
class Compteur:
def __init__(self, max:int)-> None:
self.max = max
self.nombre = 0
def __iter__(self) -> Compteur :
return self
def __next__(self) -> int :
if self.nombre < self.max:
self.nombre += 1
return self.nombre
else:
raise StopIteration
# Utilisation de l'itérateur
for i in Compteur(5):
print(i) # Affiche : 1, 2, 3, 4, 5
J’en profite pour vous parler des forward references. Il se peut, comme dans le cas ci-dessus, que vous vouliez référencer une classe avant qu’elle ne soit définie. C’est le cas pour notre fonction iter
.
Dans ce cas, il vous faut utiliser une forward reference. Pour ce faire, il faut ajouter la ligne from __future__ import annotations
en haut de votre fichier.
Générateur
Un générateur est une fonction spéciale qui utilise le mot-clé yield
pour produire une séquence de valeurs à la demande.
yield
est un mot spécial qui génère une valeur dès qu’on l’emploie, un peu comme return
, sauf que contrairement à return
, yield
suspend l'exécution de la fonction sans la terminer, conservant son état pour reprendre là où elle s'était arrêtée. Ainsi, lorsqu’on rappelle la fonction, celle-ci nous génère la valeur suivante.
L’intérêt du yield
est de permettre de ne pas stocker tous les résultats en mémoire mais de générer les données au fur et à mesure. C'est très pratique, par exemple, pour naviguer dans de grands jeux de données.
def count_up_to(max_value: int) -> Generator[int]:
count = 1
while count <= max_value:
yield count
count += 1
for number in count_up_to(5):
print(number) # Affiche : 1, 2, 3, 4, 5
En réalité, une fonction qui utilise yield
retourne un itérateur, donc vous pouvez aussi typer la fonction comme suit :
def count_up_to(max_value: int) -> Iterator[int]:
...
Souvent, on utilise des générateurs pour créer des itérateurs car ils permettent d'écrire moins de lignes de code. Et on type avec un type Iterator
quand on utilise le yield à moins de vouloir préciser des indications spécifiques sur notre générateur, comme par exemple la possibilité d’utilisé les fonctions close()
, send()
, et throw()
qu’un itérateur de base n’a pas. Si vous voulez plus de détails sur ces notions vous pouvez lire cet article de la documentation Mypy.
Les itérateurs et les générateurs ne sont pas le focus de ce cours, mais si vous voulez en savoir plus, je vous invite à parcourir ces articles :
https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/
https://python.doctor/page-iterateurs-iterator-generateur-generator-python
Ce qui conclut notre découverte du type hinting et de Mypy. Je vous ai fait voir l’essentiel, mais pour d’autres types et d’autres subtilités je vous encourage à lire la documentation Mypy.
Merci de votre lecture ;).