Annotations de types Python
Apprenez les annotations de types Python : annoter variables, fonctions et classes, utiliser le module typing et vérifier les types avec mypy.
Les annotations de types vous permettent d'associer des informations de type attendu aux variables, aux paramètres de fonctions et aux valeurs de retour. Python ne les applique pas au moment de l'exécution — ce sont des métadonnées consommées par les éditeurs, les linters et les vérificateurs de types comme mypy pour détecter les bogues avant même d'exécuter une seule ligne.
Ce chapitre couvre :
- Pourquoi les annotations de types sont importantes et quand les utiliser
- Annoter les variables et les fonctions
- Les types intégrés et le module
typing(List,Dict,Optional,Union,Tuple,Any,Callable) - La syntaxe moderne (Python 3.10+)
- Annoter les classes et
self - Les génériques et les alias de types
- L'analyse statique avec mypy
- Les pièges courants
Pourquoi les annotations de types ?
Python est typé dynamiquement : une variable peut contenir n'importe quelle valeur de n'importe quel type. Cette flexibilité est puissante, mais elle rend les grandes bases de code plus difficiles à naviguer — on ne peut pas connaître le type d'un argument de fonction simplement en lisant le site d'appel.
Les annotations de types résolvent ce problème sans renoncer au dynamisme de Python :
- Les éditeurs signalent les erreurs immédiatement. VS Code, PyCharm et autres soulignent les incompatibilités de types au fur et à mesure que vous tapez.
- Le refactoring devient plus sûr. Modifiez la signature d'une fonction et le vérificateur de types vous indique chaque site d'appel qui cesse de fonctionner.
- Le code est auto-documenté.
def greet(name: str) -> strcommunique le contrat sans docstring. - Les bibliothèques deviennent plus faciles à utiliser. Les bibliothèques typées exposent l'autocomplétion pour chaque attribut et méthode.
Les annotations de types ont été introduites dans Python 3.5 via PEP 484. La syntaxe a été affinée à chaque version majeure depuis. Les exemples ci-dessous indiquent la version Python minimale où la syntaxe est devenue disponible pour la première fois.
Annoter les variables
Ajoutez un deux-points après le nom de la variable suivi du type :
name: str = "Alice"
age: int = 30
price: float = 9.99
is_active: bool = TrueVous pouvez également déclarer le type d'une variable sans lui assigner de valeur pour l'instant. C'est ce qu'on appelle une déclaration anticipée et c'est utile à l'intérieur des classes ou au niveau du module :
user_id: int # declared but not yet assigned
user_id = 42Les annotations de types sur les variables au niveau du module n'affectent pas le comportement à l'exécution — elles sont stockées dans le dictionnaire __annotations__ du module mais ignorées par l'interpréteur.
Annoter les fonctions
Placez les annotations sur les paramètres (après le deux-points) et sur la valeur de retour (après -> avant le deux-points qui termine la signature) :
def add(a: int, b: int) -> int:
return a + b
def greet(name: str) -> str:
return f"Hello, {name}!"
def send_email(to: str, subject: str, body: str) -> None:
print(f"Sending '{subject}' to {to}")
result: int = add(3, 5)
message: str = greet("Alice")-> None signifie que la fonction n'a pas de valeur de retour significative (elle retourne None implicitement). Omettre l'annotation de retour est également valide, mais -> None explicite rend l'intention claire.
Paramètres par défaut
Les valeurs par défaut viennent après l'annotation :
def connect(host: str, port: int = 8080, secure: bool = False) -> None:
print(f"Connecting to {host}:{port} (secure={secure})")
connect("example.com") # uses defaults
connect("example.com", 443, True)*args et **kwargs
Annotez le type de l'élément, pas le type de la collection :
def total(*prices: float) -> float:
return sum(prices)
def create_user(**fields: str) -> dict:
return fields
print(round(total(9.99, 4.50, 12.00), 2)) # 26.49
print(create_user(name="Bob", role="admin"))*prices: float signifie que chaque argument positionnel est un float ; à l'exécution, prices reste un tuple ordinaire de floats. De même, **fields: str signifie que la valeur de chaque argument nommé est un str.
Le module typing
Pour tout ce qui dépasse les types intégrés de base, importez depuis le module typing (Python 3.5+). À partir de Python 3.9, de nombreux types du module typing ont été intégrés directement dans les équivalents intégrés (voir Syntaxe moderne ci-dessous).
List, Tuple, Set, Dict
from typing import List, Tuple, Set, Dict
def first_names(users: List[str]) -> str:
return users[0] if users else ""
def dimensions() -> Tuple[int, int, int]:
return (1920, 1080, 32)
def unique_tags(items: List[str]) -> Set[str]:
return set(items)
def word_count(text: str) -> Dict[str, int]:
counts: Dict[str, int] = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
return counts
print(first_names(["Alice", "Bob"])) # Alice
print(dimensions()) # (1920, 1080, 32)
print(unique_tags(["py", "web", "py"])) # {'py', 'web'}
print(word_count("one two one")) # {'one': 2, 'two': 1}Optional
Optional[X] est un raccourci pour Union[X, None]. Utilisez-le chaque fois qu'une valeur peut être absente :
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
db = {1: "Alice", 2: "Bob"}
return db.get(user_id) # returns None if not found
name = find_user(1)
if name is not None:
print(name.upper()) # ALICE
missing = find_user(99)
print(missing) # NoneUn vérificateur de types voit Optional[str] et sait que vous devez vérifier la présence de None avant d'appeler des méthodes string sur le résultat. Sans cette vérification, il signale une erreur.
Union
Union[X, Y] signifie que la valeur peut être du type X ou du type Y :
from typing import Union
def stringify(value: Union[int, float, str]) -> str:
return str(value)
print(stringify(42)) # 42
print(stringify(3.14)) # 3.14
print(stringify("hi")) # hiUnion est le plus utile lorsqu'une fonction accepte réellement plusieurs types sans relation entre eux. Si vous vous retrouvez à écrire Union[str, None], utilisez plutôt Optional[str] — c'est plus idiomatique.
Callable
Callable[[ArgTypes...], ReturnType] annote une fonction passée en argument :
from typing import Callable
def apply_twice(func: Callable[[int], int], value: int) -> int:
return func(func(value))
def double(n: int) -> int:
return n * 2
print(apply_twice(double, 3)) # 12Callable[[int], int] signifie : un callable qui prend un argument int et retourne un int. Si la liste d'arguments est complexe ou inconnue, utilisez Callable[..., ReturnType].
Any
Any est un type spécial qui désactive la vérification de types pour cette valeur. Tout type est assignable à Any et assignable depuis Any :
from typing import Any
def log(value: Any) -> None:
print(value)
log(42)
log("hello")
log([1, 2, 3])Utilisez Any avec parcimonie — c'est une trappe de sortie qui supprime la protection même que les annotations de types procurent. C'est approprié lors de l'interfaçage avec du code tiers non typé, ou lors d'une migration progressive d'une grande base de code.
Syntaxe moderne (Python 3.9+, 3.10+)
Génériques intégrés (Python 3.9+)
À partir de Python 3.9, vous pouvez utiliser les types intégrés directement comme génériques, sans importer depuis typing :
# Python 3.9+
def word_count(text: str) -> dict[str, int]:
counts: dict[str, int] = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
return counts
def first(items: list[int]) -> int | None:
return items[0] if items else None
print(word_count("cat dog cat")) # {'cat': 2, 'dog': 1}
print(first([10, 20, 30])) # 10
print(first([])) # NoneUtilisez list[str] au lieu de List[str], dict[str, int] au lieu de Dict[str, int], et ainsi de suite.
Syntaxe d'union X | Y (Python 3.10+)
Python 3.10 a introduit l'opérateur | pour les unions, remplaçant Union[X, Y] et Optional[X] :
# Python 3.10+
def parse(value: str | int | None) -> str:
if value is None:
return "nothing"
return str(value)
print(parse("hello")) # hello
print(parse(42)) # 42
print(parse(None)) # nothingstr | None est équivalent à Optional[str]. Cette syntaxe est plus propre et plus facile à lire.
Annoter les classes
Annotez les attributs d'instance à l'intérieur de __init__, et ajoutez des annotations de retour aux méthodes :
class BankAccount:
owner: str # class-level annotation (no default value)
balance: float
def __init__(self, owner: str, initial_balance: float = 0.0) -> None:
self.owner = owner
self.balance = initial_balance
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit amount must be positive.")
self.balance += amount
def withdraw(self, amount: float) -> bool:
if amount > self.balance:
return False
self.balance -= amount
return True
def __repr__(self) -> str:
return f"BankAccount(owner={self.owner!r}, balance={self.balance:.2f})"
account = BankAccount("Alice", 100.0)
account.deposit(50.0)
print(account.withdraw(30.0)) # True
print(account) # BankAccount(owner='Alice', balance=120.00)L'annotation sur self est toujours inférée — vous n'écrivez jamais self: BankAccount. Le type de retour de __init__ est toujours None.
ClassVar
Utilisez ClassVar[T] (depuis typing) pour marquer un attribut qui appartient à la classe, pas à chaque instance :
from typing import ClassVar
class Config:
MAX_RETRIES: ClassVar[int] = 3
timeout: int
def __init__(self, timeout: int) -> None:
self.timeout = timeout
print(Config.MAX_RETRIES) # 3Un vérificateur de types avertit si vous essayez de définir ClassVar sur une instance — il est destiné à être partagé au niveau de la classe.
Alias de types
Un alias de type donne à un type long ou complexe un nom plus court et plus significatif :
from typing import List, Tuple
# Simple alias
UserID = int
Filename = str
# Structured alias
Coordinates = Tuple[float, float]
Matrix = List[List[float]]
def distance(p1: Coordinates, p2: Coordinates) -> float:
return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5
print(distance((0.0, 0.0), (3.0, 4.0))) # 5.0À partir de Python 3.12, utilisez l'instruction type pour des alias explicites et inspectables :
# Python 3.12+
type Vector = list[float]
type Matrix = list[Vector]Génériques avec TypeVar
TypeVar vous permet d'écrire une seule fonction qui fonctionne avec n'importe quel type tout en préservant les relations de types :
from typing import TypeVar, List
T = TypeVar("T")
def first_item(items: List[T]) -> T:
return items[0]
x: int = first_item([1, 2, 3]) # x is int
s: str = first_item(["a", "b"]) # s is strLe vérificateur de types déduit de l'argument ce qu'est T, et transporte cette information jusqu'au type de retour. Sans TypeVar, vous devriez retourner Any et perdre la sécurité des types.
Vous pouvez contraindre TypeVar à un ensemble de types autorisés :
from typing import TypeVar
Numeric = TypeVar("Numeric", int, float)
def double(n: Numeric) -> Numeric:
return n * 2
print(double(4)) # 8 (int)
print(double(2.5)) # 5.0 (float)Vérification statique des types avec mypy
mypy est le vérificateur de types statique le plus utilisé pour Python. Installez-le avec pip :
pip install mypyExécutez-le ensuite sur un fichier :
mypy my_script.pyExemple : détecter un bogue avec mypy
Enregistrez ce qui suit sous le nom demo.py :
def greet(name: str) -> str:
return f"Hello, {name}!"
result = greet(42) # passing int instead of str
print(result.upper())L'exécution de mypy demo.py signale :
demo.py:4: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)Python lui-même exécute le code correctement (les f-strings convertissent n'importe quel type), mais mypy a détecté l'incompatibilité avant que vous n'ayez à la découvrir en production.
Options utiles de mypy
| Option | Effet |
|---|---|
--strict | Active toutes les vérifications optionnelles (recommandé pour les nouveaux projets) |
--ignore-missing-imports | Supprime les erreurs concernant les stubs tiers manquants |
--check-untyped-defs | Vérifie également les fonctions sans annotations |
--disallow-untyped-defs | Exige des annotations sur toutes les définitions de fonctions |
Un fichier mypy.ini (ou [tool.mypy] dans pyproject.toml) maintient la configuration hors de la ligne de commande :
[mypy]
strict = true
ignore_missing_imports = trueTypage progressif
Vous n'êtes pas obligé d'annoter chaque fonction en même temps. Python prend en charge le typage progressif : le code annoté et non annoté coexistent paisiblement. mypy ignore les fonctions non annotées par défaut (sauf si --check-untyped-defs est activé).
Une approche pratique pour une base de code existante :
- Ajoutez des annotations au nouveau code dès le premier jour.
- Annotez d'abord les fonctions les plus appelées ou les plus sujettes aux erreurs.
- Activez
--strictmodule par module au fur et à mesure que la couverture s'améliore. - Utilisez
Anyuniquement lorsqu'une bibliothèque tierce n'est pas typée, et ajoutez un commentaire expliquant pourquoi.
Pièges courants
Références anticipées
Si un type fait référence à une classe définie plus loin dans le même fichier, encapsulez le nom entre guillemets pour en faire une chaîne (une référence anticipée) :
class Node:
def __init__(self, value: int, next: "Node | None" = None) -> None:
self.value = value
self.next = next
head = Node(1, Node(2))
print(head.value, head.next.value) # 1 2À partir de Python 3.10+, ajoutez from __future__ import annotations en haut du fichier à la place. Cela rend toutes les annotations des chaînes paresseuses et élimine le besoin de guillemets manuels.
Annotations à l'exécution
Par défaut, les annotations dans Python 3.9 et antérieurs sont évaluées de manière eager. Cela signifie qu'une référence anticipée sans guillemets lève une NameError :
# Works (with quotes):
def clone(self: "MyClass") -> "MyClass": ...Avec from __future__ import annotations (Python 3.7+), toutes les annotations sont stockées comme des chaînes et évaluées uniquement lors de l'inspection — ce qui résout automatiquement le problème de référence anticipée.
None vs Optional
Une erreur courante est d'annoter un type de retour comme str alors que la fonction peut en réalité retourner None. Utilisez toujours Optional[str] (ou str | None) lorsque None est un retour possible :
from typing import Optional
# Wrong — mypy will flag callers that assume this is always str
def get_name(user_id: int) -> str:
if user_id == 0:
return None # type: ignore — this is the bug
# Correct
def get_name_safe(user_id: int) -> Optional[str]:
if user_id == 0:
return None
return "Alice"list vs List (compatibilité de version)
Si votre code s'exécute sur Python 3.8 ou antérieur, vous devez utiliser from typing import List et écrire List[str]. Sur Python 3.9+, list[str] fonctionne directement. Si vous devez prendre en charge les deux, utilisez soit les imports de typing, soit ajoutez from __future__ import annotations.
Référence rapide
| Annotation | Signification |
|---|---|
x: int | La variable x est un entier |
def f(a: str) -> bool | Le paramètre a est str ; la valeur de retour est bool |
-> None | La fonction ne retourne rien de significatif |
Optional[str] | str ou None |
Union[int, str] | int ou str |
list[int] / List[int] | Liste d'entiers |
dict[str, int] / Dict[str, int] | Dict associant str à int |
tuple[int, str] / Tuple[int, str] | Tuple de (int, str) |
Callable[[int], str] | Fonction prenant int, retournant str |
Any | N'importe quel type (désactive la vérification) |
ClassVar[T] | Attribut au niveau de la classe |
TypeVar("T") | Variable de type générique |
Sujets connexes
- Fonctions Python — où vivent les annotations de types sur les paramètres et les types de retour.
- Classes et objets Python — pour annoter
__init__, les méthodes et les attributs de classe. - Dataclasses Python — les annotations de types sont requises pour déclarer les champs d'une dataclass.
- Classes abstraites Python — les classes de base abstraites fonctionnent naturellement avec les annotations de types.