À quoi ça sert

Python est un langage typé dynamiquement : un nom peut contenir un int, puis un str, puis une liste, et tout marche jusqu'à ce qu'une opération impossible se déclenche à l'exécution. Les annotations de type Python sont depuis 2014 une convention optionnelle :

python
def greet(name: str) -> str:
    return f"Bonjour {name}"

Mais Python n'utilise pas ces annotations pour valider quoi que ce soit. greet(42) ne lève aucune erreur tant que la string formatée s'exécute. mypy lit le code, lit les annotations, et te dit : « ligne 5, tu passes un int là où un str est attendu ».

  • Attraper des bugs avant l'exécution — un mauvais argument passé à une fonction, un retour None oublié, un attribut qui n'existe pas.
  • Documenter le code — les types sont lisibles par l'humain (et par VS Code / IntelliJ qui font la complétion).
  • Refactorer en toute sécurité — changer la signature d'une fonction, mypy te dit immédiatement où c'est cassé.
  • Intégrer à la CI — bloquer un PR si le typage régresse.
mypy, Ruff, Pydantic… c'est quoi la différence ?

Les trois sont complémentaires. Ruff vérifie le style (import inutilisé, ligne trop longue, etc.) et reformate. Pydantic valide les données à l'exécution (un payload reçu d'une API). mypy vérifie le typage statique du code Python lui-même, sans rien exécuter. Ils répondent à trois questions différentes : « ton code est-il propre ? » (Ruff), « tes données d'entrée sont-elles correctes ? » (Pydantic), « ton code est-il cohérent côté types ? » (mypy).

Un exemple d'usage

Tu écris une fonction discount qui prend un prix et un pourcentage et renvoie le prix réduit. Tu oublies que tu reçois la réduction en string depuis un formulaire :

python (prix.py)
def discount(price: float, percent: float) -> float:
    return price * (1 - percent / 100)

reduction = "20"                     # vient d'un input HTML
print(discount(99.0, reduction))   # ← bug latent

Sans mypy, ça plante au runtime avec une TypeError cryptique au milieu du calcul. Avec mypy :

bash
mypy prix.py
prix.py:5: error: Argument 2 to "discount" has incompatible type
                  "str"; expected "float"
Found 1 error in 1 file

Tu corriges avant même de lancer le code. Pour beaucoup de classes de bugs (faute de frappe sur un attribut, retour None non géré, mauvais nombre d'arguments), c'est plus rapide qu'écrire un test.

How-to : installer et utiliser mypy

  1. Installer mypy

    Dépendance de développement uniquement (pas besoin en prod), avec UV :

    bash
    uv add --dev mypy
  2. Premier run

    bash
    uv run mypy src/         # un dossier
    uv run mypy app.py         # un fichier
    uv run mypy .              # tout le projet

    Sur un projet jamais typé, mypy par défaut est plutôt permissif : il ignore les fonctions sans annotations. Le bruit initial reste limité.

  3. Configurer mypy via pyproject.toml

    On centralise la config dans le fichier principal du projet :

    toml (pyproject.toml)
    [tool.mypy]
    python_version = "3.12"
    strict = true                      # mode strict (recommandé sur nouveaux projets)
    warn_unused_ignores = true
    warn_redundant_casts = true
    
    # Si une dépendance externe n'a pas de stubs de typage :
    [[tool.mypy.overrides]]
    module = ["some_library.*"]
    ignore_missing_imports = true
    Activer strict d'un coup ou progressivement ?

    Sur un projet neuf : strict = true dès le départ. Sur un projet existant déjà gros : commence sans strict, ajoute les options une par une (disallow_untyped_defs, warn_return_any…) pour t'éviter une montagne d'erreurs.

  4. Annoter du code Python

    La syntaxe de base se résume vite :

    python
    # Variables
    age: int = 30
    names: list[str] = []
    config: dict[str, int] = {"timeout": 30}
    
    # Optionnels (peut être None)
    result: int | None = None
    
    # Fonctions
    def add(a: int, b: int) -> int:
        return a + b
    
    # Fonction sans retour utile
    def log(msg: str) -> None:
        print(msg)
  5. Types plus avancés

    python
    from typing import Callable, Literal, TypedDict
    from collections.abc import Iterable
    
    # Valeurs limitées
    status: Literal["draft", "published"] = "draft"
    
    # Itérables (n'importe quoi qu'on peut for-iterer)
    def total(values: Iterable[float]) -> float:
        return sum(values)
    
    # Fonctions passées en argument
    def retry(fn: Callable[[int], str]) -> str:
        return fn(3)
    
    # Dict structuré
    class User(TypedDict):
        name: str
        age: int
  6. Ignorer une ligne (en dernier recours)

    python
    result = legacy_func(x)  # type: ignore[no-untyped-call]

    On indique toujours le code d'erreur entre crochets (mypy le donne dans son message), pour ne pas masquer d'autres erreurs futures sur la même ligne. Avec warn_unused_ignores = true dans la config, mypy te signale quand un type: ignore n'est plus utile.

  7. Intégrer dans pre-commit et la CI

    On ajoute un hook dans .pre-commit-config.yaml :

    yaml
    - repo: https://github.com/pre-commit/mirrors-mypy
      rev: v1.13.0
      hooks:
        - id: mypy
          additional_dependencies: [pydantic, types-requests]

    additional_dependencies est important : le hook tourne dans son propre venv, donc il a besoin que tu listes les libs typées (Pydantic, FastAPI, etc.) pour pouvoir résoudre les types. Sinon le hook tombe à côté.

    Côté CI, ajouter un step uv run mypy . avant les tests fait le même boulot comme filet de sécurité.

Aide-mémoire

bash
uv add --dev mypy
uv run mypy src/                    # lancer
uv run mypy --strict src/
uv run mypy --show-error-codes …    # afficher les codes
python (annotations courantes)
name: str; age: int; ok: bool
items: list[str]; pairs: dict[str, int]
opt: int | None = None
status: Literal["on", "off"]

def f(x: int) -> str: ...
def g(*args: int, **kw: str) -> None: ...
toml (config stricte type)
[tool.mypy]
python_version = "3.12"
strict = true
warn_unused_ignores = true

mypy et le reste de l'écosystème

  • Ruff — Ruff vérifie le style et certaines erreurs basiques (variable inutilisée, import non utilisé). mypy se concentre sur les types. On les utilise les deux ensemble, pas l'un ou l'autre.
  • Pydantic — partenariat naturel. Pydantic valide à l'exécution, mypy vérifie statiquement. Si tu déclares def create_user(data: UserCreate) -> User, mypy s'assure que tu passes bien un UserCreate, et Pydantic valide les champs au moment du parsing.
  • FastAPI — FastAPI utilise massivement le typage Python pour générer le routing, la validation et la doc. Le typage est la config — mypy te protège des incohérences à grande échelle.
  • pre-commit — hook mypy à ajouter au config existant. Bloque le commit si du typage est cassé.
  • VS Code — l'extension Pylance fait du type-checking en temps réel (basé sur Pyright, cousin de mypy). En CI on garde mypy comme source de vérité commune.
  • pytest — complémentaire. mypy attrape les bugs « ce code n'est pas cohérent ». pytest attrape « ce code ne fait pas ce qu'il devrait ». Les deux sont nécessaires.
Les stubs (types-*)

Certaines libs anciennes (requests, etc.) ne livrent pas leurs types. La communauté maintient des paquets séparés types-requests, types-PyYAML… à installer en dev. Si mypy se plaint d'Library stubs not installed, c'est la piste à suivre.

Pour aller plus loin