Indices de type Python : Un guide pratique des annotations de type
Updated on
Le typage dynamique de Python est flexible, mais il crée de vrais problèmes à grande échelle. Les fonctions acceptent silencieusement des types incorrects et produisent des erreurs confuses loin du bug réel. Le refactoring casse du code dans des endroits imprévisibles. Lire le code de quelqu'un d'autre signifie deviner quels types circulent dans chaque variable, chaque paramètre de fonction, chaque valeur de retour. À mesure que les bases de code grandissent, ces suppositions se transforment en bugs, et ces bugs se transforment en heures de débogage.
Le coût se cumule dans les environnements d'équipe. Sans informations de type, chaque appel de fonction nécessite de lire l'implémentation pour comprendre ce qu'elle attend et ce qu'elle retourne. Les revues de code ralentissent. Les nouveaux membres de l'équipe mettent plus de temps à monter en compétence. Les outils automatisés ne peuvent pas aider car ils n'ont aucune information de type sur laquelle s'appuyer.
Les indices de type Python résolvent cela en ajoutant des annotations de type optionnelles que les IDE, les vérificateurs de type et les humains peuvent vérifier. Ils documentent votre intention directement dans le code, détectent des catégories entières de bugs avant l'exécution, et débloquent des fonctionnalités puissantes d'éditeur comme l'autocomplétion et la détection d'erreurs en ligne. Ce guide couvre tout, des annotations de base aux patterns avancés utilisés dans les bases de code Python de production.
Que sont les indices de type ?
Les indices de type sont des annotations optionnelles qui spécifient les types attendus des variables, des paramètres de fonction et des valeurs de retour. Ils ont été introduits dans la PEP 484 (opens in a new tab) (Python 3.5) et ont été affinés à travers des PEPs ultérieures incluant la PEP 526 (annotations de variables), la PEP 604 (syntaxe union) et la PEP 612 (spécifications de paramètres).
Le mot clé est optionnel. Les indices de type n'affectent pas le comportement à l'exécution. Python ne les applique pas pendant l'exécution. Ils existent pour trois publics : les développeurs lisant le code, les IDE fournissant l'autocomplétion et la détection d'erreurs, et les vérificateurs de type statiques comme mypy qui analysent le code sans l'exécuter.
# Sans indices de type - qu'est-ce que cette fonction attend ?
def process_data(data, threshold, output):
...
# Avec indices de type - instantanément clair
def process_data(data: list[float], threshold: float, output: str) -> dict[str, float]:
...La seconde version vous dit tout en un coup d'œil : data est une liste de floats, threshold est un float, output est une chaîne, et la fonction retourne un dictionnaire associant des chaînes à des floats. Pas besoin de lire l'implémentation ou de tracer les sites d'appel.
Annotations de type de base
Annotations de variables
Les annotations de variables utilisent la syntaxe à deux-points introduite dans la PEP 526 (Python 3.6) :
# Annotations de variables de base
name: str = "Alice"
age: int = 30
height: float = 5.9
is_active: bool = True
raw_data: bytes = b"hello"
# Annotations sans assignation (déclaration seule)
username: str
count: intVous pouvez annoter des variables sans leur assigner de valeur. C'est utile dans les corps de classe et les blocs conditionnels où la variable se voit assignée plus tard.
Paramètres de fonction et types de retour
Les annotations de fonction utilisent deux-points pour les paramètres et -> pour les types de retour :
def greet(name: str) -> str:
return f"Hello, {name}!"
def calculate_average(numbers: list[float]) -> float:
return sum(numbers) / len(numbers)
def save_record(record: dict[str, str], overwrite: bool = False) -> None:
"""Les fonctions qui ne retournent rien utilisent -> None."""
...L'annotation -> None communique explicitement qu'une fonction effectue une action sans retourner de valeur significative. C'est important car cela distingue les retours None intentionnels des instructions return oubliées.
Types intégrés
Les types intégrés de Python correspondent directement aux indices de type :
| Type | Exemple | Description |
|---|---|---|
int | count: int = 10 | Entiers |
float | price: float = 9.99 | Nombres à virgule flottante |
str | name: str = "Bob" | Chaînes de texte |
bool | active: bool = True | Valeurs booléennes |
bytes | data: bytes = b"\x00" | Séquences d'octets |
None | result: None = None | Le singleton None |
def parse_config(path: str, encoding: str = "utf-8") -> dict[str, str]:
config: dict[str, str] = {}
with open(path, encoding=encoding) as f:
for line in f:
key, _, value = line.partition("=")
config[key.strip()] = value.strip()
return configTypes de collection
Syntaxe moderne (Python 3.9+)
À partir de Python 3.9, vous pouvez utiliser les types de collection intégrés directement comme types génériques :
# Listes
scores: list[int] = [95, 87, 92]
names: list[str] = ["Alice", "Bob"]
# Dictionnaires
user_ages: dict[str, int] = {"Alice": 30, "Bob": 25}
config: dict[str, list[str]] = {"servers": ["a.com", "b.com"]}
# Tuples - longueur fixe avec types spécifiques
point: tuple[float, float] = (3.14, 2.72)
record: tuple[str, int, bool] = ("Alice", 30, True)
# Tuples de longueur variable (tous même type)
values: tuple[int, ...] = (1, 2, 3, 4, 5)
# Sets et frozensets
tags: set[str] = {"python", "typing"}
constants: frozenset[int] = frozenset({1, 2, 3})Syntaxe héritée (Python 3.5-3.8)
Avant Python 3.9, vous deviez importer les types génériques depuis le module typing :
from typing import List, Dict, Tuple, Set, FrozenSet
scores: List[int] = [95, 87, 92]
user_ages: Dict[str, int] = {"Alice": 30}
point: Tuple[float, float] = (3.14, 2.72)
tags: Set[str] = {"python", "typing"}Tableau de comparaison des syntaxes
| Type | Python 3.9+ | Python 3.5-3.8 |
|---|---|---|
| Liste | list[int] | typing.List[int] |
| Dictionnaire | dict[str, int] | typing.Dict[str, int] |
| Tuple (fixe) | tuple[str, int] | typing.Tuple[str, int] |
| Tuple (variable) | tuple[int, ...] | typing.Tuple[int, ...] |
| Set | set[str] | typing.Set[str] |
| FrozenSet | frozenset[int] | typing.FrozenSet[int] |
| Type | type[MyClass] | typing.Type[MyClass] |
Utilisez la syntaxe moderne dès que votre projet cible Python 3.9 ou ultérieur. Elle est plus propre et ne nécessite pas d'imports.
Collections imbriquées
Les types de collection se composent naturellement pour des structures de données complexes :
# Matrice : liste de listes de floats
matrix: list[list[float]] = [[1.0, 2.0], [3.0, 4.0]]
# Mapping des utilisateurs vers leurs listes de scores
gradebook: dict[str, list[int]] = {
"Alice": [95, 87, 92],
"Bob": [78, 85, 90],
}
# Configuration : dictionnaires imbriqués
app_config: dict[str, dict[str, str | int]] = {
"database": {"host": "localhost", "port": 5432},
"cache": {"host": "redis.local", "port": 6379},
}Types Optional et Union
Types Optional
Une valeur qui pourrait être None est annotée avec Optional ou la syntaxe union :
from typing import Optional
# Syntaxe pré-3.10
def find_user(user_id: int) -> Optional[str]:
"""Retourne le nom d'utilisateur ou None si non trouvé."""
users = {1: "Alice", 2: "Bob"}
return users.get(user_id)
# Syntaxe Python 3.10+ (préférée)
def find_user(user_id: int) -> str | None:
"""Retourne le nom d'utilisateur ou None si non trouvé."""
users = {1: "Alice", 2: "Bob"}
return users.get(user_id)Optional[str] est exactement équivalent à str | None. La syntaxe avec pipe est plus lisible et ne nécessite pas d'import.
Types Union
Quand une valeur peut être l'un de plusieurs types :
from typing import Union
# Syntaxe pré-3.10
def format_value(value: Union[int, float, str]) -> str:
return str(value)
# Syntaxe Python 3.10+ (préférée)
def format_value(value: int | float | str) -> str:
return str(value)
# Pattern courant : accepter plusieurs formats d'entrée
def load_data(source: str | Path) -> list[dict[str, str]]:
path = Path(source) if isinstance(source, str) else source
with open(path) as f:
return json.load(f)Comparaison de la syntaxe Union
| Pattern | Pré-3.10 | Python 3.10+ |
|---|---|---|
| Nullable | Optional[str] | str | None |
| Deux types | Union[int, str] | int | str |
| Multiple | Union[int, float, str] | int | float | str |
| Union nullable | Optional[Union[int, str]] | int | str | None |
Types avancés du module typing
Any
Any désactive la vérification de type pour une valeur spécifique. Utilisez-le avec parcimonie comme échappatoire :
from typing import Any
def log_event(event: str, payload: Any) -> None:
"""Accepte tout type de payload - utile pour le logging générique."""
print(f"[{event}] {payload}")
# Any est compatible avec tous les types
log_event("click", {"x": 100, "y": 200})
log_event("error", 404)
log_event("message", "hello")TypeVar et Generic
TypeVar crée des variables de type génériques pour les fonctions et classes qui fonctionnent avec plusieurs types tout en préservant les relations de type :
from typing import TypeVar
T = TypeVar("T")
def first_element(items: list[T]) -> T:
"""Retourne le premier élément, préservant son type."""
return items[0]
# Les vérificateurs de type infèrent le bon type de retour
name = first_element(["Alice", "Bob"]) # type: str
score = first_element([95, 87, 92]) # type: int
# TypeVar borné - restriction à des types spécifiques
Numeric = TypeVar("Numeric", int, float)
def add(a: Numeric, b: Numeric) -> Numeric:
return a + bCréation de classes génériques :
from typing import TypeVar, Generic
T = TypeVar("T")
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def peek(self) -> T:
return self._items[-1]
def is_empty(self) -> bool:
return len(self._items) == 0
# Utilisation avec des types spécifiques
int_stack: Stack[int] = Stack()
int_stack.push(42)
value: int = int_stack.pop()
str_stack: Stack[str] = Stack()
str_stack.push("hello")Callable
Callable annote les paramètres de fonction et les callbacks :
from typing import Callable
# Fonction qui prend un callback
def apply_operation(
values: list[float],
operation: Callable[[float], float]
) -> list[float]:
return [operation(v) for v in values]
# Utilisation
import math
results = apply_operation([1.0, 4.0, 9.0], math.sqrt)
# results: [1.0, 2.0, 3.0]
# Signatures Callable plus complexes
Comparator = Callable[[str, str], int]
EventHandler = Callable[[str, dict[str, str]], None]
def sort_with_comparator(
items: list[str],
compare: Comparator
) -> list[str]:
import functools
return sorted(items, key=functools.cmp_to_key(compare))Literal
Literal restreint les valeurs à des constantes spécifiques :
from typing import Literal
def set_log_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]) -> None:
print(f"Log level set to {level}")
set_log_level("INFO") # OK
set_log_level("VERBOSE") # Erreur de type : pas un littéral valide
# Utile pour les paramètres de mode
def read_file(
path: str,
mode: Literal["text", "binary"] = "text"
) -> str | bytes:
if mode == "text":
return open(path).read()
return open(path, "rb").read()TypedDict
TypedDict définit la forme des dictionnaires avec des clés spécifiques et des types de valeur :
from typing import TypedDict, NotRequired
class UserProfile(TypedDict):
name: str
email: str
age: int
bio: NotRequired[str] # Clé optionnelle (Python 3.11+)
def display_user(user: UserProfile) -> str:
return f"{user['name']} ({user['email']}), age {user['age']}"
# Le vérificateur de type valide la structure
user: UserProfile = {
"name": "Alice",
"email": "alice@example.com",
"age": 30,
}
display_user(user) # OKProtocol (Sous-typage structurel)
Protocol définit des interfaces basées sur la structure plutôt que sur l'héritage. Si un objet a les méthodes requises, il satisfait le protocol :
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self, x: int, y: int) -> None: ...
class Circle:
def draw(self, x: int, y: int) -> None:
print(f"Drawing circle at ({x}, {y})")
class Square:
def draw(self, x: int, y: int) -> None:
print(f"Drawing square at ({x}, {y})")
def render(shape: Drawable, x: int, y: int) -> None:
shape.draw(x, y)
# Les deux fonctionnent sans hériter de Drawable
render(Circle(), 10, 20)
render(Square(), 30, 40)
# runtime_checkable permet les vérifications isinstance
print(isinstance(Circle(), Drawable)) # TrueTypeAlias
TypeAlias crée des alias de type explicites pour les types complexes :
from typing import TypeAlias
# Alias simples
UserId: TypeAlias = int
JsonDict: TypeAlias = dict[str, "JsonValue"]
JsonValue: TypeAlias = str | int | float | bool | None | list["JsonValue"] | JsonDict
# Les alias complexes simplifient les signatures
Matrix: TypeAlias = list[list[float]]
Callback: TypeAlias = Callable[[str, int], bool]
Config: TypeAlias = dict[str, str | int | list[str]]
def transform_matrix(m: Matrix, factor: float) -> Matrix:
return [[cell * factor for cell in row] for row in m]
# Python 3.12+ utilise l'instruction type
# type Vector = list[float]Vérification de type avec mypy
Installation et exécution de mypy
mypy est le vérificateur de type statique le plus largement utilisé pour Python :
# Installer mypy
# pip install mypy
# Vérifier un fichier unique
# mypy script.py
# Vérifier un projet entier
# mypy src/
# Vérifier avec une version Python spécifique
# mypy --python-version 3.10 src/Configuration
Configurez mypy dans pyproject.toml ou mypy.ini pour des paramètres globaux au projet :
# Configuration pyproject.toml
# [tool.mypy]
# python_version = "3.10"
# warn_return_any = true
# warn_unused_configs = true
# disallow_untyped_defs = true
# check_untyped_defs = true
# no_implicit_optional = true
# strict_equality = true
# Remplacements par module
# [[tool.mypy.overrides]]
# module = "third_party_lib.*"
# ignore_missing_imports = trueFlags mypy courants
| Flag | Effet |
|---|---|
--strict | Active toutes les vérifications strictes (recommandé pour les nouveaux projets) |
--ignore-missing-imports | Ignore les erreurs pour les bibliothèques tierces non typées |
--disallow-untyped-defs | Exige des annotations de type sur toutes les fonctions |
--no-implicit-optional | Ne traite pas la valeur par défaut None comme Optional |
--warn-return-any | Avertit quand on retourne Any depuis des fonctions typées |
--show-error-codes | Affiche les codes d'erreur pour chaque erreur (utile pour la suppression) |
Correction des erreurs mypy courantes
# Erreur : Type de valeur de retour incompatible (got "Optional[str]", expected "str")
# Correction : Gérer le cas None
def get_name(user_id: int) -> str:
result = lookup(user_id) # retourne str | None
if result is None:
raise ValueError(f"User {user_id} not found")
return result # mypy sait que result est un str ici
# Erreur : Item "None" of "Optional[str]" has no attribute "upper"
# Correction : Réduire le type d'abord
def format_name(name: str | None) -> str:
if name is not None:
return name.upper()
return "UNKNOWN"
# Erreur : Need type annotation for variable
# Correction : Ajouter une annotation explicite
items: list[str] = [] # Pas juste : items = []
# Erreur : Incompatible types in assignment
# Correction : Utiliser Union ou corriger le type
value: int | str = 42
value = "hello" # OK avec le type union
# Supprimer des erreurs spécifiques si nécessaire
x = some_untyped_function() # type: ignore[no-untyped-call]Indices de type en pratique
FastAPI et Pydantic
FastAPI utilise les indices de type pour piloter la validation des requêtes, la sérialisation et la documentation :
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class UserCreate(BaseModel):
name: str
email: str
age: int
tags: list[str] = []
class UserResponse(BaseModel):
id: int
name: str
email: str
@app.post("/users", response_model=UserResponse)
async def create_user(user: UserCreate) -> UserResponse:
# FastAPI valide le corps de la requête contre UserCreate
# et sérialise la réponse pour correspondre à UserResponse
return UserResponse(id=1, name=user.name, email=user.email)Pydantic utilise les indices de type pour valider automatiquement les données, convertir les types et générer des schémas JSON. Les annotations de type ne sont pas juste de la documentation -- elles pilotent le comportement à l'exécution.
Data Science : Typage des DataFrames
Les indices de type sont de plus en plus importants dans les workflows de data science :
import pandas as pd
from typing import Any
# Typage de DataFrame de base
def clean_dataframe(df: pd.DataFrame) -> pd.DataFrame:
return df.dropna().reset_index(drop=True)
# Utilisation de pandera pour la validation de schéma
# pip install pandera
import pandera as pa
from pandera.typing import DataFrame, Series
class SalesSchema(pa.DataFrameModel):
product: Series[str]
quantity: Series[int] = pa.Field(ge=0)
price: Series[float] = pa.Field(gt=0)
date: Series[pd.Timestamp]
@pa.check_types
def process_sales(df: DataFrame[SalesSchema]) -> DataFrame[SalesSchema]:
"""Traitement de DataFrame avec vérification de type."""
return df[df["quantity"] > 0]Avantages des IDE
Les indices de type débloquent des fonctionnalités puissantes d'IDE dans tous les éditeurs majeurs :
| Fonctionnalité | Sans indices de type | Avec indices de type |
|---|---|---|
| Autocomplétion | Suggestions génériques | Complétions contextuelles pour le type exact |
| Détection d'erreurs | Erreurs à l'exécution seulement | Erreurs en ligne avant l'exécution |
| Refactoring | Recherche-remplacement manuel | Renommage et refactoring automatisés sûrs |
| Documentation | Nécessite des docstrings ou lecture de la source | Hover montre les types en ligne |
| Navigation | Recherche textuelle | Saut vers les définitions et implémentations typées |
Avantages des indices de type à travers les outils
| Outil | Comment il utilise les indices de type |
|---|---|
| mypy | Vérification de type statique, détecte les bugs avant l'exécution |
| Pyright/Pylance | Vérification de type et autocomplétion VS Code |
| FastAPI | Validation requête/réponse et documentation API |
| Pydantic | Validation de données, sérialisation, gestion des paramètres |
| SQLAlchemy 2.0 | Colonnes mappées, types de résultats de requêtes |
| pytest | Inférence de type des plugins, typage des fixtures |
| attrs/dataclasses | Génération automatique de __init__, __repr__, __eq__ |
Aide-mémoire des indices de type
Voici un tableau de référence rapide des annotations les plus courantes :
| Annotation | Signification | Exemple |
|---|---|---|
int | Entier | count: int = 0 |
float | Flottant | price: float = 9.99 |
str | Chaîne | name: str = "Alice" |
bool | Booléen | active: bool = True |
bytes | Octets | data: bytes = b"" |
None | Type None | -> None |
list[int] | Liste d'entiers | scores: list[int] = [] |
dict[str, int] | Dict associant str à int | ages: dict[str, int] = {} |
tuple[str, int] | Tuple de longueur fixe | pair: tuple[str, int] |
tuple[int, ...] | Tuple de longueur variable | nums: tuple[int, ...] |
set[str] | Set de chaînes | tags: set[str] = set() |
str | None | Chaîne nullable (3.10+) | name: str | None = None |
Optional[str] | Chaîne nullable (pré-3.10) | name: Optional[str] = None |
int | str | Int ou string (3.10+) | value: int | str |
Union[int, str] | Int ou string (pré-3.10) | value: Union[int, str] |
Any | Tout type (échappatoire) | data: Any |
Callable[[int], str] | Type de fonction | fn: Callable[[int], str] |
Literal["a", "b"] | Valeurs spécifiques uniquement | mode: Literal["r", "w"] |
TypeVar("T") | Variable de type générique | T = TypeVar("T") |
ClassVar[int] | Variable de niveau classe | count: ClassVar[int] = 0 |
Final[str] | Ne peut pas être réassigné | NAME: Final = "app" |
TypeAlias | Alias de type explicite | UserId: TypeAlias = int |
Erreurs courantes
| Erreur | Problème | Correction |
|---|---|---|
def f(x: list) | Type d'élément manquant | def f(x: list[int]) |
items = [] | Le type ne peut pas être inféré | items: list[str] = [] |
def f(x: int = None) | La valeur par défaut est None mais le type est int | def f(x: int | None = None) |
from typing import List (3.9+) | Import inutile | Utiliser list[int] directement |
def f(x: dict) | Types de clé/valeur manquants | def f(x: dict[str, int]) |
isinstance(x, list[int]) | Impossible d'utiliser des génériques avec isinstance | isinstance(x, list) |
def f() -> True | Utilise une valeur, pas un type | def f() -> bool |
Annoter self | Redondant, mypy l'infère | Omettre l'annotation self |
x: str = 42 | Mauvaise annotation | Faire correspondre l'annotation au type réel |
Surutilisation de Any | Défait le but du typage | Utiliser des types spécifiques ou TypeVar |
# Erreur : valeur par défaut mutable avec indice de type
def bad_append(item: str, items: list[str] = []) -> list[str]:
items.append(item) # Valeur par défaut mutable partagée !
return items
# Correction : utiliser None comme valeur par défaut
def good_append(item: str, items: list[str] | None = None) -> list[str]:
if items is None:
items = []
items.append(item)
return itemsExemple pratique : Un pipeline de données typé
Voici un exemple complet montrant comment les indices de type fonctionnent ensemble dans un scénario de traitement de données réel. Ce pattern est courant dans les workflows de data science et d'analyse :
from typing import TypedDict, Callable, TypeAlias
from pathlib import Path
import csv
# Définir les formes de données avec TypedDict
class RawRecord(TypedDict):
name: str
value: str
category: str
class ProcessedRecord(TypedDict):
name: str
value: float
category: str
normalized: float
# Alias de type pour les fonctions de transformation
Transform: TypeAlias = Callable[[list[ProcessedRecord]], list[ProcessedRecord]]
def load_csv(path: Path) -> list[RawRecord]:
"""Charger des données CSV avec sortie typée."""
records: list[RawRecord] = []
with open(path) as f:
reader = csv.DictReader(f)
for row in reader:
records.append(RawRecord(
name=row["name"],
value=row["value"],
category=row["category"],
))
return records
def parse_records(raw: list[RawRecord]) -> list[ProcessedRecord]:
"""Convertir les enregistrements bruts en enregistrements typés."""
max_value = max(float(r["value"]) for r in raw) or 1.0
return [
ProcessedRecord(
name=r["name"],
value=float(r["value"]),
category=r["category"],
normalized=float(r["value"]) / max_value,
)
for r in raw
]
def filter_by_category(
records: list[ProcessedRecord],
category: str
) -> list[ProcessedRecord]:
"""Filtrer les enregistrements, entrée et sortie entièrement typées."""
return [r for r in records if r["category"] == category]
def apply_transforms(
records: list[ProcessedRecord],
transforms: list[Transform]
) -> list[ProcessedRecord]:
"""Appliquer une chaîne de fonctions de transformation typées."""
result = records
for transform in transforms:
result = transform(result)
return result
# Utilisation
raw = load_csv(Path("data.csv"))
processed = parse_records(raw)
filtered = filter_by_category(processed, "electronics")Chaque fonction dans ce pipeline a des types d'entrée et de sortie clairs. Un vérificateur de type peut vérifier que la sortie d'une fonction correspond à l'entrée de la suivante. Si quelqu'un change la structure de ProcessedRecord, mypy signale chaque endroit qui doit être mis à jour.
Visualisation de données typées avec PyGWalker
Quand vous travaillez avec des DataFrames typés dans des workflows de data science, PyGWalker (opens in a new tab) transforme vos DataFrames pandas en une interface de visualisation interactive de type Tableau. Il fonctionne bien aux côtés des pipelines vérifiés par type car les données structurées que vous produisez avec un typage approprié alimentent directement des graphiques et dashboards explorables :
import pandas as pd
import pygwalker as pyg
# Votre pipeline typé produit des données propres et structurées
data: list[ProcessedRecord] = parse_records(raw)
df = pd.DataFrame(data)
# PyGWalker le rend comme une visualisation interactive
walker = pyg.walk(df)Pour des environnements de notebooks interactifs, RunCell (opens in a new tab) fournit une expérience Jupyter alimentée par l'IA où le code vérifié par type et l'exploration visuelle des données fonctionnent ensemble de manière transparente.
FAQ
Les indices de type affectent-ils les performances de Python ?
Non. L'exécution Python ignore complètement les indices de type. Ils sont stockés comme métadonnées sur les fonctions et variables mais jamais évalués pendant l'exécution normale. Il y a un overhead mémoire négligeable pour stocker les annotations, mais aucun impact sur la vitesse d'exécution. Des frameworks comme Pydantic et FastAPI lisent bien les annotations au démarrage pour construire la logique de validation, mais c'est un comportement de framework, pas une caractéristique du langage Python.
Les indices de type sont-ils obligatoires en Python ?
Non. Les indices de type sont complètement optionnels. Python reste un langage dynamiquement typé, et le code s'exécute de manière identique avec ou sans annotations. Cependant, les indices de type sont fortement recommandés pour tout projet avec plus d'un développeur ou toute base de code nécessitant une maintenance à long terme. Les projets Python majeurs comme FastAPI, SQLAlchemy et Django s'appuient de plus en plus sur les indices de type.
Quelle est la différence entre les indices de type et la vérification de type ?
Les indices de type sont les annotations que vous écrivez dans votre code, comme x: int ou -> str. La vérification de type est le processus de vérification que votre code est cohérent avec ces annotations. La vérification de type est effectuée par des outils externes comme mypy, Pyright ou Pylance -- pas par Python lui-même. Vous pouvez avoir des indices de type sans exécuter un vérificateur de type, et les indices fournissent toujours de la valeur via l'autocomplétion IDE et la documentation.
Devrais-je utiliser typing.List ou list pour les indices de type ?
Utilisez list[int] en minuscules si votre projet cible Python 3.9 ou ultérieur. typing.List[int] est la syntaxe héritée requise pour Python 3.5-3.8. La syntaxe en minuscules est plus propre, ne nécessite pas d'import, et est l'approche recommandée pour l'avenir. La même règle s'applique à dict vs typing.Dict, tuple vs typing.Tuple, et set vs typing.Set.
Quel est le meilleur vérificateur de type pour Python ?
mypy est le vérificateur de type le plus établi et largement utilisé pour Python. Pyright (utilisé par l'extension Pylance de VS Code) est plus rapide et détecte certaines erreurs que mypy manque. Les deux sont activement maintenus. Pour la plupart des projets, utilisez celui qui s'intègre le mieux à votre éditeur. mypy est le standard pour les pipelines CI. Pyright fournit la meilleure expérience temps réel dans VS Code. Vous pouvez exécuter les deux dans un projet sans conflits.
Conclusion
Les indices de type Python comblent le fossé entre la flexibilité dynamique de Python et les garanties de sécurité des langages statiquement typés. Ils détectent les bugs avant l'exécution, rendent le code auto-documentant, et débloquent des fonctionnalités puissantes d'IDE qui accélèrent le développement.
Commencez par les bases : annotez les paramètres de fonction, les types de retour et les variables complexes. Utilisez list[int], dict[str, str], et str | None pour les types courants. Exécutez mypy dans votre pipeline CI pour détecter automatiquement les erreurs de type. À mesure que votre confiance grandit, adoptez des patterns avancés comme TypedDict, Protocol et Generic pour modéliser des types de domaine complexes.
L'investissement paie vite. Une seule annotation de type qui empêche un bug de production justifie des heures d'effort de frappe. Les équipes rapportent une montée en compétence plus rapide, un refactoring plus sûr, et moins de surprises à l'exécution après avoir adopté les indices de type. Avec des frameworks comme FastAPI et Pydantic construisant leur conception entière autour des annotations de type, Python typé n'est pas une pratique de niche -- c'est la direction vers laquelle l'écosystème se dirige.