Introduction aux Types Python¶
Python supporte des annotations de type (ou type hints) optionnelles.
Ces annotations de type constituent une syntaxe spéciale qui permet de déclarer le type d'une variable.
En déclarant les types de vos variables, cela permet aux différents outils comme les éditeurs de texte d'offrir un meilleur support.
Ce chapitre n'est qu'un tutoriel rapide / rappel sur les annotations de type Python. Seulement le minimum nécessaire pour les utiliser avec FastAPI sera couvert... ce qui est en réalité très peu.
FastAPI est totalement basé sur ces annotations de type, qui lui donnent de nombreux avantages.
Mais même si vous n'utilisez pas ou n'utiliserez jamais FastAPI, vous pourriez bénéficier d'apprendre quelques choses sur ces dernières.
Note
Si vous êtes un expert Python, et que vous savez déjà tout sur les annotations de type, passez au chapitre suivant.
Motivations¶
Prenons un exemple simple :
def get_full_name(first_name, last_name):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
Exécuter ce programe affiche :
John Doe
La fonction :
- Prend un
first_name
et unlast_name
. - Convertit la première lettre de chaque paramètre en majuscules grâce à
title()
. - Concatène les résultats avec un espace entre les deux.
def get_full_name(first_name, last_name):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
Limitations¶
C'est un programme très simple.
Mais maintenant imaginez que vous l'écriviez de zéro.
À un certain point vous auriez commencé la définition de la fonction, vous aviez les paramètres prêts.
Mais vous aviez besoin de "cette méthode qui convertit la première lettre en majuscule".
Était-ce upper
? uppercase
? first_uppercase
? capitalize
?
Vous essayez donc d'utiliser le vieil ami du programmeur, l'auto-complétion de l'éditeur.
Vous écrivez le premier paramètre, first_name
, puis un point (.
) et appuyez sur Ctrl+Espace
pour déclencher l'auto-complétion.
Mais malheureusement, rien d'utile n'en résulte :
Ajouter des types¶
Modifions une seule ligne de la version précédente.
Nous allons changer seulement cet extrait, les paramètres de la fonction, de :
first_name, last_name
à :
first_name: str, last_name: str
C'est tout.
Ce sont des annotations de types :
def get_full_name(first_name: str, last_name: str):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
À ne pas confondre avec la déclaration de valeurs par défaut comme ici :
first_name="john", last_name="doe"
C'est une chose différente.
On utilise un deux-points (:
), et pas un égal (=
).
Et ajouter des annotations de types ne crée normalement pas de différence avec le comportement qui aurait eu lieu si elles n'étaient pas là.
Maintenant, imaginez que vous êtes en train de créer cette fonction, mais avec des annotations de type cette fois.
Au même moment que durant l'exemple précédent, vous essayez de déclencher l'auto-complétion et vous voyez :
Vous pouvez donc dérouler les options jusqu'à trouver la méthode à laquelle vous pensiez.
Plus de motivations¶
Cette fonction possède déjà des annotations de type :
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + age
return name_with_age
Comme l'éditeur connaît le type des variables, vous n'avez pas seulement l'auto-complétion, mais aussi de la détection d'erreurs :
Maintenant que vous avez connaissance du problème, convertissez age
en chaîne de caractères grâce à str(age)
:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + str(age)
return name_with_age
Déclarer des types¶
Vous venez de voir là où les types sont généralement déclarés : dans les paramètres de fonctions.
C'est aussi ici que vous les utiliseriez avec FastAPI.
Types simples¶
Vous pouvez déclarer tous les types de Python, pas seulement str
.
Comme par exemple :
int
float
bool
bytes
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
return item_a, item_b, item_c, item_d, item_d, item_e
Types génériques avec des paramètres de types¶
Il existe certaines structures de données qui contiennent d'autres valeurs, comme dict
, list
, set
et tuple
. Et les valeurs internes peuvent elles aussi avoir leurs propres types.
Pour déclarer ces types et les types internes, on utilise le module standard de Python typing
.
Il existe spécialement pour supporter ces annotations de types.
List
¶
Par exemple, définissons une variable comme list
de str
.
Importez List
(avec un L
majuscule) depuis typing
.
from typing import List
def process_items(items: List[str]):
for item in items:
print(item)
Déclarez la variable, en utilisant la syntaxe des deux-points (:
).
Et comme type, mettez List
.
Les listes étant un type contenant des types internes, mettez ces derniers entre crochets ([
, ]
) :
from typing import List
def process_items(items: List[str]):
for item in items:
print(item)
Astuce
Ces types internes entre crochets sont appelés des "paramètres de type".
Ici, str
est un paramètre de type passé à List
.
Ce qui signifie : "la variable items
est une list
, et chacun de ses éléments a pour type str
.
En faisant cela, votre éditeur pourra vous aider, même pendant que vous traitez des éléments de la liste.
Sans types, c'est presque impossible à réaliser.
Vous remarquerez que la variable item
n'est qu'un des éléments de la list items
.
Et pourtant, l'éditeur sait qu'elle est de type str
et pourra donc vous aider à l'utiliser.
Tuple
et Set
¶
C'est le même fonctionnement pour déclarer un tuple
ou un set
:
from typing import Set, Tuple
def process_items(items_t: Tuple[int, int, str], items_s: Set[bytes]):
return items_t, items_s
Dans cet exemple :
- La variable
items_t
est untuple
avec 3 éléments, unint
, un deuxièmeint
, et unstr
. - La variable
items_s
est unset
, et chacun de ses éléments est de typebytes
.
Dict
¶
Pour définir un dict
, il faut lui passer 2 paramètres, séparés par une virgule (,
).
Le premier paramètre de type est pour les clés et le second pour les valeurs du dictionnaire (dict
).
from typing import Dict
def process_items(prices: Dict[str, float]):
for item_name, item_price in prices.items():
print(item_name)
print(item_price)
Dans cet exemple :
- La variable
prices
est de typedict
:- Les clés de ce dictionnaire sont de type
str
. - Les valeurs de ce dictionnaire sont de type
float
.
- Les clés de ce dictionnaire sont de type
Optional
¶
Vous pouvez aussi utiliser Optional
pour déclarer qu'une variable a un type, comme str
mais qu'il est "optionnel" signifiant qu'il pourrait aussi être None
.
from typing import Optional
def say_hi(name: Optional[str] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
Utiliser Optional[str]
plutôt que str
permettra à l'éditeur de vous aider à détecter les erreurs où vous supposeriez qu'une valeur est toujours de type str
, alors qu'elle pourrait aussi être None
.
Types génériques¶
Les types qui peuvent contenir des paramètres de types entre crochets, comme :
List
Tuple
Set
Dict
Optional
- ...et d'autres.
sont appelés des types génériques ou Generics.
Classes en tant que types¶
Vous pouvez aussi déclarer une classe comme type d'une variable.
Disons que vous avez une classe Person
, avec une variable name
:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
Vous pouvez ensuite déclarer une variable de type Person
:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
Et vous aurez accès, encore une fois, au support complet offert par l'éditeur :
Les modèles Pydantic¶
Pydantic est une bibliothèque Python pour effectuer de la validation de données.
Vous déclarez la forme de la donnée avec des classes et des attributs.
Chaque attribut possède un type.
Puis vous créez une instance de cette classe avec certaines valeurs et Pydantic validera les valeurs, les convertira dans le type adéquat (si c'est nécessaire et possible) et vous donnera un objet avec toute la donnée.
Ainsi, votre éditeur vous offrira un support adapté pour l'objet résultant.
Extrait de la documentation officielle de Pydantic :
from datetime import datetime
from typing import List, Union
from pydantic import BaseModel
class User(BaseModel):
id: int
name = "John Doe"
signup_ts: Union[datetime, None] = None
friends: List[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
Info
Pour en savoir plus à propos de Pydantic, allez jeter un coup d'oeil à sa documentation.
FastAPI est basé entièrement sur Pydantic.
Vous verrez bien plus d'exemples de son utilisation dans Tutoriel - Guide utilisateur.
Les annotations de type dans FastAPI¶
FastAPI utilise ces annotations pour faire différentes choses.
Avec FastAPI, vous déclarez des paramètres grâce aux annotations de types et vous obtenez :
- du support de l'éditeur
- de la vérification de types
...et FastAPI utilise ces mêmes déclarations pour :
- Définir les prérequis : depuis les paramètres de chemins des requêtes, les entêtes, les corps, les dépendances, etc.
- Convertir des données : depuis la requête vers les types requis.
- Valider des données : venant de chaque requête :
- Générant automatiquement des erreurs renvoyées au client quand la donnée est invalide.
- Documenter l'API avec OpenAPI :
- ce qui ensuite utilisé par les interfaces utilisateur automatiques de documentation interactive.
Tout cela peut paraître bien abstrait, mais ne vous inquiétez pas, vous verrez tout ça en pratique dans Tutoriel - Guide utilisateur.
Ce qu'il faut retenir c'est qu'en utilisant les types standard de Python, à un seul endroit (plutôt que d'ajouter plus de classes, de décorateurs, etc.), FastAPI fera une grande partie du travail pour vous.
Info
Si vous avez déjà lu le tutoriel et êtes revenus ici pour voir plus sur les types, une bonne ressource est la "cheat sheet" de mypy
.