mypy
Le type-checker statique de référence pour Python. Vérifie que les
annotations de type (name: str, price: float)
sont cohérentes sans exécuter le code — il attrape
une catégorie de bugs que Ruff et pytest ne voient pas.
À 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 :
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
Noneoublié, 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.
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 :
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 :
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
-
Installer mypy
Dépendance de développement uniquement (pas besoin en prod), avec UV :
bashuv add --dev mypy -
Premier run
bashuv run mypy src/ # un dossier uv run mypy app.py # un fichier uv run mypy . # tout le projetSur un projet jamais typé, mypy par défaut est plutôt permissif : il ignore les fonctions sans annotations. Le bruit initial reste limité.
-
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 = trueActiver strict d'un coup ou progressivement ?Sur un projet neuf :
strict = truedès le départ. Sur un projet existant déjà gros : commence sansstrict, ajoute les options une par une (disallow_untyped_defs,warn_return_any…) pour t'éviter une montagne d'erreurs. -
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) -
Types plus avancés
pythonfrom 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 -
Ignorer une ligne (en dernier recours)
pythonresult = 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 = truedans la config, mypy te signale quand untype: ignoren'est plus utile. -
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_dependenciesest 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
uv add --dev mypy
uv run mypy src/ # lancer
uv run mypy --strict src/
uv run mypy --show-error-codes … # afficher les codes
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: ...
[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 unUserCreate, 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.
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
- Site officiel : mypy.readthedocs.io
- Cheat sheet officiel : mypy.readthedocs.io/en/stable/cheat_sheet_py3.html
- PEP 484 (origine du typage Python) : peps.python.org/pep-0484
- Pyright (alternative, plus rapide, utilisé par Pylance) : github.com/microsoft/pyright