---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Annotations de types et mypy

```{code-cell} python
:tags: [hide-input]

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
```

## Pourquoi annoter les types ?

Python est un langage à typage **dynamique** : le type d'une variable est déterminé à l'exécution, et rien n'empêche de réassigner une variable à un objet d'un type différent. Cette flexibilité est une force, mais elle a un coût : les erreurs de type ne sont détectées qu'à l'exécution, parfois longtemps après l'introduction du bug, parfois dans des chemins de code rarement empruntés.

Les **annotations de types** (*type hints*), introduites par la PEP 484 (Python 3.5), sont la réponse de Python à ce problème. Elles permettent d'**annoter** le type attendu des variables, des paramètres et des valeurs de retour, sans changer le comportement dynamique du langage. Python n'évalue pas ces annotations à l'exécution : elles sont destinées aux **outils d'analyse statique** (notamment `mypy`, `pyright`, `pylance`) et aux IDE.

Les bénéfices sont multiples et concrets :

1. **Documentation vivante.** Une signature `def calculer_tva(prix: float, taux: float) -> float` communique instantanément les types attendus et le type retourné, sans nécessiter un docstring.
2. **Détection précoce des erreurs.** Un outil comme `mypy` peut signaler qu'on passe une chaîne là où un entier est attendu, avant même d'exécuter le code. Ces erreurs auraient pu passer des semaines inaperçues.
3. **Autocomplétion améliorée.** Les IDE peuvent proposer des méthodes et attributs pertinents car ils connaissent le type exact de chaque variable.
4. **Refactorisation sûre.** Changer le type d'une valeur retournée par une fonction déclenche des erreurs statiques dans tous les endroits qui en dépendent, rendant la refactorisation bien plus sûre.
5. **Évolution du code.** Sur les grandes bases de code et en équipe, les annotations de types servent de contrat entre les composants, réduisant les malentendus sur les interfaces.

Il est important de comprendre que les annotations sont **optionnelles et progressives** : on peut annoter uniquement les fonctions publiques les plus critiques d'un projet, et ajouter des annotations ailleurs au fur et à mesure. Le code non annoté fonctionne exactement comme avant.

## Syntaxe des annotations

### Variables

```{code-cell} python
# Annotation de variable (depuis Python 3.6)
nom: str = "Alice"
age: int = 30
prix: float = 19.99
actif: bool = True

# Annotation sans valeur initiale (déclare le type sans assigner)
identifiant: int
```

### Paramètres et valeurs de retour

La syntaxe pour annoter une fonction utilise `:` pour les paramètres et `->` pour la valeur de retour :

```{code-cell} python
def saluer(prenom: str, titre: str = "M.") -> str:
    return f"Bonjour, {titre} {prenom} !"

def somme(valeurs: list[int]) -> int:
    return sum(valeurs)

# Fonction sans valeur de retour : annoter avec None
def afficher(message: str) -> None:
    print(message)

print(saluer("Dupont"))
print(somme([1, 2, 3, 4, 5]))
```

### Union de types : `X | Y` vs `Union[X, Y]`

Avant Python 3.10, exprimer qu'une valeur peut être de l'un ou l'autre type nécessitait `Union` du module `typing`. Depuis Python 3.10, la syntaxe `X | Y` est disponible directement :

```{code-cell} python
from typing import Union

# Avant Python 3.10
def ancienne_syntaxe(x: Union[int, str]) -> Union[int, str]:
    return x

# Depuis Python 3.10 (PEP 604)
def nouvelle_syntaxe(x: int | str) -> int | str:
    return x

# Type optionnel : None possible (équivalent à X | None)
from typing import Optional

def trouver(cle: str) -> Optional[str]:  # str | None depuis Python 3.10
    return None

print(nouvelle_syntaxe(42))
print(nouvelle_syntaxe("bonjour"))
```

## Le module `typing`

Le module `typing` est la bibliothèque centrale pour les annotations de types complexes. Bien que Python 3.9+ permette d'utiliser directement `list[int]`, `dict[str, int]`, etc. (sans majuscules), le module `typing` reste utile pour les constructions avancées.

```{code-cell} python
from typing import (
    Optional, Union, List, Dict, Tuple, Set,
    Any, Callable, TypeVar, Sequence, Iterable
)

# List, Dict, Tuple (deprecated depuis 3.9, mais encore courants)
def traiter(elements: List[int]) -> Dict[str, int]:
    return {"total": sum(elements), "max": max(elements)}

# Tuple avec types fixes
def coordonnees() -> Tuple[float, float]:
    return (3.14, 2.71)

# Callable : type d'une fonction
def appliquer(f: Callable[[int, int], int], a: int, b: int) -> int:
    return f(a, b)

print(appliquer(lambda x, y: x + y, 3, 4))
print(traiter([1, 2, 3, 4, 5]))
```

**`Any`** est le type d'échappement : une valeur de type `Any` est compatible avec tous les autres types. À utiliser avec parcimonie, car il désactive la vérification statique pour cette valeur.

**`TypeVar`** représente un **paramètre de type** générique. Il exprime que plusieurs paramètres ou le retour d'une fonction partagent le même type, sans le fixer à l'avance :

```{code-cell} python
from typing import TypeVar

T = TypeVar('T')

def premier(liste: list[T]) -> T:
    """Retourne le premier élément, quel que soit son type."""
    if not liste:
        raise ValueError("La liste est vide.")
    return liste[0]

print(premier([1, 2, 3]))        # int
print(premier(["a", "b", "c"]))  # str
print(premier([3.14, 2.71]))     # float
```

## Annotations avancées

### `Literal`

`Literal` exprime qu'une valeur doit être exactement une des valeurs littérales spécifiées :

```{code-cell} python
from typing import Literal

Direction = Literal["nord", "sud", "est", "ouest"]

def deplacer(direction: Direction, pas: int = 1) -> str:
    return f"Déplacement de {pas} vers le {direction}."

print(deplacer("nord", 3))
# mypy signalerait une erreur si on passait "haut" ici
```

### `Final`

`Final` indique qu'une variable ne doit pas être réassignée. C'est l'équivalent de `const` dans d'autres langages :

```{code-cell} python
from typing import Final

TAUX_TVA: Final = 0.20
MAX_TENTATIVES: Final[int] = 3

print(f"TVA : {TAUX_TVA * 100:.0f}%")
print(f"Max tentatives : {MAX_TENTATIVES}")
```

### `TypedDict`

`TypedDict` permet d'annoter des dictionnaires dont les clés et les types de valeurs sont connus à l'avance :

```{code-cell} python
from typing import TypedDict

class Utilisateur(TypedDict):
    nom: str
    age: int
    email: str
    actif: bool

def afficher_utilisateur(u: Utilisateur) -> None:
    statut = "actif" if u["actif"] else "inactif"
    print(f"{u['nom']} ({u['age']} ans) — {u['email']} [{statut}]")

alice: Utilisateur = {
    "nom": "Alice Martin",
    "age": 32,
    "email": "alice@exemple.fr",
    "actif": True,
}
afficher_utilisateur(alice)
```

### `Protocol`

`Protocol` (PEP 544) implémente le **typage structurel** (*duck typing* statique) : une classe satisfait un protocole si elle possède les méthodes et attributs requis, sans avoir à hériter explicitement du protocole. C'est l'équivalent des interfaces en Go ou en TypeScript :

```{code-cell} python
from typing import Protocol

class Dessinable(Protocol):
    def dessiner(self) -> str:
        ...

class Cercle:
    def dessiner(self) -> str:
        return "○"

class Rectangle:
    def dessiner(self) -> str:
        return "□"

class Triangle:
    def dessiner(self) -> str:
        return "△"

def afficher_forme(forme: Dessinable) -> None:
    print(forme.dessiner())

# Toutes ces classes satisfont le protocole Dessinable
# sans en hériter explicitement
for forme in [Cercle(), Rectangle(), Triangle()]:
    afficher_forme(forme)
```

### `overload`

`@overload` permet de déclarer plusieurs signatures pour une même fonction, utile quand le type de retour dépend du type des arguments :

```{code-cell} python
from typing import overload

@overload
def doubler(x: int) -> int: ...
@overload
def doubler(x: str) -> str: ...

def doubler(x):  # Implémentation réelle (sans annotation)
    if isinstance(x, int):
        return x * 2
    return x + x

print(doubler(5))      # int → int
print(doubler("abc"))  # str → str
```

## Génériques

Les **classes génériques** permettent d'écrire du code paramétré par un type, comme `list[T]` ou `dict[K, V]` dans la bibliothèque standard.

```{code-cell} python
from typing import TypeVar, Generic

T = TypeVar('T')

class Pile(Generic[T]):
    """Pile LIFO générique."""

    def __init__(self) -> None:
        self._elements: list[T] = []

    def empiler(self, element: T) -> None:
        self._elements.append(element)

    def depiler(self) -> T:
        if not self._elements:
            raise IndexError("La pile est vide.")
        return self._elements.pop()

    def sommet(self) -> T:
        if not self._elements:
            raise IndexError("La pile est vide.")
        return self._elements[-1]

    def __len__(self) -> int:
        return len(self._elements)

    def __repr__(self) -> str:
        return f"Pile({self._elements})"


pile_int: Pile[int] = Pile()
pile_int.empiler(1)
pile_int.empiler(2)
pile_int.empiler(3)
print(f"Pile : {pile_int}, sommet : {pile_int.sommet()}")
print(f"Dépilé : {pile_int.depiler()}, reste : {pile_int}")
```

### Variance

La **variance** décrit comment les relations de sous-typage se propagent à travers les types génériques. Par défaut, un `TypeVar` est **invariant** : `Pile[int]` n'est ni un sous-type ni un super-type de `Pile[float]`. On peut spécifier :

- `covariant=True` : `Pile[int]` est un sous-type de `Pile[float]` si `int` est un sous-type de `float`. Utilisé pour les types en lecture seule.
- `contravariant=True` : inverse. Utilisé pour les types en écriture seule.

```python
from typing import TypeVar

T_co = TypeVar('T_co', covariant=True)

class LecteurGenerique(Generic[T_co]):
    """Producteur de T, donc covariant."""
    def lire(self) -> T_co: ...
```

## `mypy`

`mypy` est le vérificateur de types statique de référence pour Python. Il analyse le code source et signale les incohérences de types **sans exécuter le code**.

### Installation et utilisation de base

```bash
# Installation
pip install mypy
# ou avec uv
uv add --dev mypy

# Vérification d'un fichier
mypy mon_module.py

# Vérification de tout un projet
mypy src/
```

### Configuration `mypy.ini`

`mypy` se configure via `mypy.ini`, `pyproject.toml` (section `[tool.mypy]`) ou `.mypy.ini` :

```ini
# mypy.ini
[mypy]
python_version = 3.12
strict = true
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = false

[mypy-bibliotheque_externe.*]
ignore_missing_imports = true
```

Le mode **`--strict`** active un ensemble de vérifications supplémentaires : interdiction de `Any` implicite, vérification des types de retour même pour les fonctions non annotées appelées depuis du code annoté, etc. C'est le mode recommandé pour les nouveaux projets.

### Lire les erreurs de mypy

```{code-cell} python
:tags: [hide-input]

fig, ax = plt.subplots(figsize=(13, 7))
ax.set_xlim(0, 13)
ax.set_ylim(0, 7)
ax.axis('off')
ax.set_title("Évolution de la syntaxe de typage Python", fontsize=14,
             fontweight='bold', pad=12)

versions = ["Python 3.5", "Python 3.9", "Python 3.10", "Python 3.12"]
xs = [1.5, 4.5, 7.5, 10.5]
couleurs = ['#3498db', '#27ae60', '#e67e22', '#8e44ad']

for x, ver, col in zip(xs, versions, couleurs):
    rect = patches.FancyBboxPatch(
        (x - 1.3, 6.0), 2.6, 0.7,
        boxstyle="round,pad=0.12", linewidth=2,
        edgecolor=col, facecolor=col, alpha=0.9
    )
    ax.add_patch(rect)
    ax.text(x, 6.35, ver, ha='center', va='center',
            fontsize=11, fontweight='bold', color='white')

exemples = [
    # (y, Python 3.5, Python 3.9, Python 3.10, Python 3.12)
    (5.2, "List[int]", "list[int]", "list[int]", "list[int]"),
    (4.4, "Dict[str, int]", "dict[str, int]", "dict[str, int]", "dict[str, int]"),
    (3.6, "Optional[str]", "Optional[str]", "str | None", "str | None"),
    (2.8, "Union[int, str]", "Union[int, str]", "int | str", "int | str"),
    (2.0, "Tuple[int, ...]", "tuple[int, ...]", "tuple[int, ...]", "tuple[int, ...]"),
    (1.2, "Type[T]", "type[T]", "type[T]", "type[T]"),
    (0.4, "—", "—", "—", "TypeVarTuple (3.11+)"),
]

labels_gauche = [
    "Liste d'entiers",
    "Dict str→int",
    "Optionnel",
    "Union",
    "Tuple variadique",
    "Méta-type",
    "Nouveau (3.11+)",
]

for i, (y, *valeurs) in enumerate(exemples):
    for j, (x, val) in enumerate(zip(xs, valeurs)):
        col = couleurs[j]
        est_moderne = val in ["str | None", "int | str"] and j >= 2
        bg_col = col if est_moderne else '#ecf0f1'
        txt_col = 'white' if est_moderne else '#2c3e50'
        rect = patches.FancyBboxPatch(
            (x - 1.25, y - 0.28), 2.5, 0.52,
            boxstyle="round,pad=0.06", linewidth=1.2,
            edgecolor=col, facecolor=bg_col, alpha=0.9
        )
        ax.add_patch(rect)
        ax.text(x, y, val, ha='center', va='center',
                fontsize=8, color=txt_col, fontfamily='monospace',
                fontweight='bold' if est_moderne else 'normal')
    ax.text(0.1, y, labels_gauche[i], ha='left', va='center',
            fontsize=8, color='#555555', style='italic')

plt.tight_layout()
plt.show()
```

Les erreurs de `mypy` suivent un format standard :

```
fichier.py:12: error: Argument 1 to "saluer" has incompatible type "int"; expected "str"  [arg-type]
```

Le code entre crochets (`[arg-type]`, `[return-value]`, `[assignment]`, etc.) identifie la catégorie d'erreur. On peut supprimer une erreur spécifique avec `# type: ignore[code]` :

```python
resultat = fonction_externe()  # type: ignore[no-untyped-call]
```

### Intégration VS Code

L'extension officielle Python de VS Code utilise `pylance` (basé sur `pyright`) pour la vérification de types en temps réel. On peut aussi configurer `mypy` comme vérificateur alternatif via les paramètres `python.analysis.typeCheckingMode` et `mypy.enabled`.

```{prf:remark}
:label: remark-14-01
`mypy` et `pyright` peuvent produire des diagnostics légèrement différents sur les mêmes fichiers car ils implémentent la spécification de typage de façon indépendante. `pyright` (utilisé par pylance dans VS Code) est généralement plus rapide et plus strict ; `mypy` est plus configurable et souvent considéré comme la référence. Pour la cohérence en CI, choisissez l'un des deux et tenez-vous-y.
```

```{prf:example} Stratégie d'adoption progressive
:label: example-14-01
Pour adopter les types dans un projet existant sans friction excessive :

1. Commencer par annoter les **interfaces publiques** : fonctions et méthodes publiques des modules principaux.
2. Activer `mypy` en mode souple (sans `--strict`) sur le projet entier.
3. Corriger les erreurs module par module, en ordre de priorité.
4. Activer progressivement des options plus strictes (`--disallow-untyped-defs`, `--no-implicit-optional`).
5. Atteindre le mode `--strict` complet sur les modules les plus critiques.

Cette approche évite le "big bang" d'une migration totale et permet à l'équipe de s'approprier les annotations graduellement.
```

## Résumé

Ce chapitre a exploré le système d'annotations de types de Python et son écosystème d'outils :

- Les annotations de types sont **optionnelles et progressives** ; elles ne changent pas le comportement dynamique de Python mais servent aux outils statiques et à la documentation.
- La **syntaxe de base** utilise `:` pour les paramètres et variables, `->` pour les retours. Depuis Python 3.10, `X | Y` remplace `Union[X, Y]` et `X | None` remplace `Optional[X]`.
- Le module **`typing`** fournit `TypeVar`, `Any`, `Callable`, `TypedDict`, `Protocol`, `Literal`, `Final`, `overload` et d'autres constructions avancées.
- Les **génériques** permettent d'écrire des classes et fonctions paramétrées par un type (`Generic[T]`), avec un contrôle fin de la variance.
- **`mypy`** est l'outil de vérification statique de référence, configurable via `mypy.ini` ou `pyproject.toml`. Le mode `--strict` est recommandé pour les nouveaux projets.

Dans le chapitre suivant, nous verrons comment écrire et organiser des **tests automatisés** avec `pytest`, l'outil de test de facto en Python.
