Type hints dans Python et vérification avec mypy
1. Vue d’ensemble
Python est un langage avec typage dynamique, ce qui signifie que vous n’avez pas besoin de déclarer les types de données à l’avance. C’est flexible mais cela peut facilement causer des erreurs à l’exécution lorsque le projet grandit. Les type hints (PEP 484) vous permettent d’annoter les types de données sans modifier le comportement à l’exécution, et mypy lira ces annotations pour détecter les erreurs de type directement pendant que vous codez.
Après avoir lu cet article, vous saurez comment écrire des type hints pour les variables, fonctions, classes, utiliser Optional, Union, TypedDict, les génériques, et intégrer mypy dans votre processus de vérification de code.
2. Type hints de base
2.1. Déclaration de type pour les variables et fonctions
def calculate_total(price: float, quantity: int) -> float:
return price * quantity
customer_name: str = "Nguyễn Văn A"
total: float = calculate_total(price=15.5, quantity=3)
print(total)
price: float,quantity: intsont les types des paramètres d’entrée.-> floatest le type de la valeur retournée.customer_name: strest l’annotation pour la variable, ce qui aide l’IDE pour les suggestions et mypy pour vérifier.
46.5
Remarque: Les type hints ne font PAS de conversion de type à l’exécution. Si vous appelez
calculate_total("10", 2), Python continue à s’exécuter normalement car l’interpréteur ignore l’annotation — seuls les outils comme mypy signaleront l’erreur.
2.2. Installation de mypy
pip install mypy
Vérifier le fichier :
mypy order_service.py
S’il n’y a pas d’erreur, mypy affiche :
Success: no issues found in 1 source file
3. Optional et Union pour les valeurs pouvant être None
De nombreuses fonctions retournent None quand elles ne trouvent pas de données. Utilisez Optional pour l’indiquer explicitement.
from typing import Optional
class Customer:
def __init__(self, customer_id: int, email: str) -> None:
self.customer_id = customer_id
self.email = email
def find_customer(customer_id: int) -> Optional[Customer]:
database = {1: Customer(1, "a@example.com")}
return database.get(customer_id)
customer = find_customer(2)
if customer is not None:
print(customer.email)
Optional[Customer]est équivalent àUnion[Customer, None].- Si vous appelez
customer.emailsans vérifierNoned’abord, mypy signalera l’erreurItem "None" of "Optional[Customer]" has no attribute "email".
from typing import Union
def format_id(value: Union[int, str]) -> str:
return str(value).zfill(6)
Union[int, str] permet au paramètre d’être soit int, soit str. À partir de Python 3.10, vous pouvez écrire plus simplement int | str à la place de Union[int, str], et Customer | None à la place de Optional[Customer].
4. Types de collection : list, dict, tuple
from typing import Dict, List, Tuple
def summarize_orders(order_totals: List[float]) -> Tuple[float, int]:
return sum(order_totals), len(order_totals)
def build_price_table(items: Dict[str, float]) -> Dict[str, float]:
return {name: price * 1.1 for name, price in items.items()}
List[float]signifie une liste de nombres à virgule flottante.Tuple[float, int]signifie un tuple avec exactement 2 éléments : unfloatpuis unint.Dict[str, float]est un dictionnaire avec des clés de typestret des valeurs de typefloat.
Remarque: À partir de Python 3.9, vous pouvez utiliser directement
list[float],dict[str, float]sans importer detyping. Utiliseztyping.List/typing.Dictprincipalement pour la compatibilité avec Python 3.8 et versions antérieures.
5. TypedDict pour les données en forme de dict structuré
Quand vous travaillez avec du JSON ou des dicts avec un schéma fixe, TypedDict aide mypy à vérifier chaque clé.
from typing import TypedDict
class OrderPayload(TypedDict):
order_id: int
customer_email: str
total: float
def create_order(payload: OrderPayload) -> str:
return f"Order #{payload['order_id']} - {payload['customer_email']} - {payload['total']}"
payload: OrderPayload = {
"order_id": 101,
"customer_email": "b@example.com",
"total": 250.0,
}
print(create_order(payload))
Si vous manquez la clé total ou tapez mal "totl", mypy signale :
error: Missing key "total" for TypedDict "OrderPayload"
C’est un grand avantage par rapport à l’utilisation de Dict[str, Any] — vous contrôlez la structure des données tout en gardant un dict Python pur à l’exécution.
6. Génériques avec TypeVar
Quand vous écrivez une fonction ou une classe qui fonctionne avec plusieurs types de données, utilisez TypeVar pour maintenir la relation entre le type d’entrée et de sortie.
from typing import TypeVar, List
T = TypeVar("T")
def first_item(items: List[T]) -> T:
return items[0]
first_name: str = first_item(["Hùng", "Lan"])
first_number: int = first_item([10, 20, 30])
TypeVar("T")crée un placeholder de type.- Quand vous appelez
first_item(["Hùng", "Lan"]), mypy déduit queT = str, donc le résultat retourné eststr. - Quand vous appelez avec
List[int],T = int. Grâce à cela, mypy connaît le type de retour exact selon chaque appel, au lieu d’être vague commeAny.
7. Class et Protocol à la place d’interface
Python n’a pas d’interface comme Java, mais Protocol (PEP 544) vous permet de définir le typage structural — vérifier “a-t-il cette méthode ?” au lieu de devoir hériter.
from typing import Protocol
class PaymentGateway(Protocol):
def charge(self, amount: float) -> bool:
...
class VnPayGateway:
def charge(self, amount: float) -> bool:
print(f"Charging {amount} via VNPay")
return True
def process_payment(gateway: PaymentGateway, amount: float) -> bool:
return gateway.charge(amount)
process_payment(VnPayGateway(), 199000.0)
VnPayGateway n’a pas besoin d’hériter de PaymentGateway — il suffit qu’il ait la méthode charge(self, amount: float) -> bool pour que mypy l’accepte comme satisfaisant le Protocol. C’est une approche très “Pythonic”, qui préserve le duck typing tout en ayant une vérification statique.
8. Configuration de mypy pour le projet
Ajoutez la dépendance à requirements-dev.txt :
mypy==1.11.2
Créez un fichier de configuration mypy.ini à la racine du projet :
[mypy]
python_version = 3.12
disallow_untyped_defs = True
warn_return_any = True
warn_unused_ignores = True
ignore_missing_imports = True
disallow_untyped_defs = Trueoblige toutes les fonctions à avoir des type hints, évitant les oublis.warn_return_any = Truesignale quand une fonction retourneAnysilencieusement.ignore_missing_imports = Trueest utile quand vous utilisez des bibliothèques tierces sans type stub.
Exécutez la vérification sur l’ensemble du projet :
mypy src/
9. Écrire des tests pour le comportement avec type hints
Les type hints ne remplacent pas les tests, mais aident les tests à être plus clairs sur les types de données d’entrée/sortie. Utilisez pytest :
<!-- requirements-dev.txt format équivalent, juste pour illustrer la structure de la dépendance -->
pytest==8.3.2
from order_service import calculate_total
def test_calculate_total_returns_correct_sum() -> None:
result = calculate_total(price=20.0, quantity=5)
assert result == 100.0
def test_calculate_total_with_zero_quantity() -> None:
result = calculate_total(price=50.0, quantity=0)
assert result == 0.0
Exécutez les tests :
pytest -v
test_order_service.py::test_calculate_total_returns_correct_sum PASSED
test_order_service.py::test_calculate_total_with_zero_quantity PASSED
Vous pouvez combiner les deux étapes dans votre CI : exécutez mypy d’abord pour attraper les erreurs de type, puis pytest pour vérifier la logique métier. Les deux outils se complètent — mypy attrape les erreurs “mauvais type de données”, pytest attrape les erreurs “mauvaise logique”.
10. Erreurs courantes et quand mypy n’est pas nécessaire
- Oublier de mettre à jour le type hint quand on change la logique : par exemple, une fonction passe de retourner
Optional[Customer]à toujours retournerCustomer, mais vous oubliez de modifier l’annotation — mypy ne signalera pas l’erreur si le code correspond toujours au type ancien plus large, ce qui rend les informations d’annotation inexactes. - Utiliser
Anypartout : déclarerdef process(data: Any) -> Anyprive mypy de la capacité à vérifier, ce qui revient à désactiver le typage statique à cet endroit. - Annotation incorrecte avec les bibliothèques tierces sans stub : de nombreux anciens packages n’ont pas publié de type stub, mypy signalera
error: Cannot find implementation or library stub. Vous gérez cela avecignore_missing_importsou en installanttypes-<package>s’il existe. - Quand NE PAS utiliser mypy strictement : pour un petit script, un script unique à usage unique (throwaway script), ou un prototype rapide pour tester une idée — forcer des type hints complets peut ralentir votre vitesse d’écriture sans bénéfice proportionnel.
Remarque: mypy fait uniquement une analyse statique (static analysis) du code source, il ne l’exécute pas. Il ne peut pas attraper les erreurs qui dépendent des données d’exécution, par exemple vérifier si une valeur lue depuis un fichier de configuration ou une API externe a vraiment le type déclaré.
11. Conclusion
Les type hints rendent le code Python plus lisible, plus facile à maintenir et réduisent les erreurs dues aux confusions de type, tandis que mypy transforme ces annotations en outil de vérification réel au lieu d’être juste de la documentation. Vous devriez commencer à appliquer progressivement aux fonctions publiques, modules importants, puis étendre graduellement à tout le projet avec la configuration disallow_untyped_defs. Combiner mypy dans votre CI avec des tests utilisant pytest est l’approche la plus pratique pour garder la flexibilité de Python tout en ayant un niveau de sécurité proche des langages à typage statique.