Skip to content

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: int

Vous 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 :

TypeExempleDescription
intcount: int = 10Entiers
floatprice: float = 9.99Nombres à virgule flottante
strname: str = "Bob"Chaînes de texte
boolactive: bool = TrueValeurs booléennes
bytesdata: bytes = b"\x00"Séquences d'octets
Noneresult: None = NoneLe 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 config

Types 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

TypePython 3.9+Python 3.5-3.8
Listelist[int]typing.List[int]
Dictionnairedict[str, int]typing.Dict[str, int]
Tuple (fixe)tuple[str, int]typing.Tuple[str, int]
Tuple (variable)tuple[int, ...]typing.Tuple[int, ...]
Setset[str]typing.Set[str]
FrozenSetfrozenset[int]typing.FrozenSet[int]
Typetype[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

PatternPré-3.10Python 3.10+
NullableOptional[str]str | None
Deux typesUnion[int, str]int | str
MultipleUnion[int, float, str]int | float | str
Union nullableOptional[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 + b

Cré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)  # OK

Protocol (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))  # True

TypeAlias

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 = true

Flags mypy courants

FlagEffet
--strictActive toutes les vérifications strictes (recommandé pour les nouveaux projets)
--ignore-missing-importsIgnore les erreurs pour les bibliothèques tierces non typées
--disallow-untyped-defsExige des annotations de type sur toutes les fonctions
--no-implicit-optionalNe traite pas la valeur par défaut None comme Optional
--warn-return-anyAvertit quand on retourne Any depuis des fonctions typées
--show-error-codesAffiche 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 typeAvec indices de type
AutocomplétionSuggestions génériquesComplétions contextuelles pour le type exact
Détection d'erreursErreurs à l'exécution seulementErreurs en ligne avant l'exécution
RefactoringRecherche-remplacement manuelRenommage et refactoring automatisés sûrs
DocumentationNécessite des docstrings ou lecture de la sourceHover montre les types en ligne
NavigationRecherche textuelleSaut vers les définitions et implémentations typées

Avantages des indices de type à travers les outils

OutilComment il utilise les indices de type
mypyVérification de type statique, détecte les bugs avant l'exécution
Pyright/PylanceVérification de type et autocomplétion VS Code
FastAPIValidation requête/réponse et documentation API
PydanticValidation de données, sérialisation, gestion des paramètres
SQLAlchemy 2.0Colonnes mappées, types de résultats de requêtes
pytestInférence de type des plugins, typage des fixtures
attrs/dataclassesGé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 :

AnnotationSignificationExemple
intEntiercount: int = 0
floatFlottantprice: float = 9.99
strChaînename: str = "Alice"
boolBooléenactive: bool = True
bytesOctetsdata: bytes = b""
NoneType None-> None
list[int]Liste d'entiersscores: list[int] = []
dict[str, int]Dict associant str à intages: dict[str, int] = {}
tuple[str, int]Tuple de longueur fixepair: tuple[str, int]
tuple[int, ...]Tuple de longueur variablenums: tuple[int, ...]
set[str]Set de chaînestags: set[str] = set()
str | NoneChaîne nullable (3.10+)name: str | None = None
Optional[str]Chaîne nullable (pré-3.10)name: Optional[str] = None
int | strInt ou string (3.10+)value: int | str
Union[int, str]Int ou string (pré-3.10)value: Union[int, str]
AnyTout type (échappatoire)data: Any
Callable[[int], str]Type de fonctionfn: Callable[[int], str]
Literal["a", "b"]Valeurs spécifiques uniquementmode: Literal["r", "w"]
TypeVar("T")Variable de type génériqueT = TypeVar("T")
ClassVar[int]Variable de niveau classecount: ClassVar[int] = 0
Final[str]Ne peut pas être réassignéNAME: Final = "app"
TypeAliasAlias de type expliciteUserId: TypeAlias = int

Erreurs courantes

ErreurProblèmeCorrection
def f(x: list)Type d'élément manquantdef 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 intdef f(x: int | None = None)
from typing import List (3.9+)Import inutileUtiliser list[int] directement
def f(x: dict)Types de clé/valeur manquantsdef f(x: dict[str, int])
isinstance(x, list[int])Impossible d'utiliser des génériques avec isinstanceisinstance(x, list)
def f() -> TrueUtilise une valeur, pas un typedef f() -> bool
Annoter selfRedondant, mypy l'infèreOmettre l'annotation self
x: str = 42Mauvaise annotationFaire correspondre l'annotation au type réel
Surutilisation de AnyDéfait le but du typageUtiliser 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 items

Exemple 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.

📚