def calculer_total(prix, quantite, tva):
return prix * quantite * (1 + tva)
# Que veut-on passer ? Des nombres ? Des listes ?
# Aucun moyen de savoir sans lire le corps de la fonction.48 Type hints et mypy
Python est dynamiquement typé : on ne déclare pas les types. C’est pratique pour prototyper, mais dangereux à grande échelle — un bug de type se découvre seulement à l’exécution. Depuis Python 3.5, on peut ajouter des annotations de types (type hints) qui, sans ralentir le code, permettent à des outils comme mypy de détecter les erreurs avant exécution. C’est une compétence très valorisée en entreprise.
48.1 Pourquoi annoter ?
Sans annotations :
Avec annotations :
def calculer_total(prix: float, quantite: int, tva: float) -> float:
"""Calcule le total TTC."""
return prix * quantite * (1 + tva)Maintenant :
- La signature est auto-documentée : on voit les types attendus et le retour.
- L’éditeur (VS Code, PyCharm) peut vérifier pendant que vous tapez.
mypypeut analyser tout le projet avant exécution.
def additionner(a: int, b: int) -> int:
return a + b
# Python NE VÉRIFIE PAS que a et b sont des int
print(additionner("Hello ", "World")) # fonctionne !
print(additionner(3.14, 2.5)) # fonctionne aussiHello World
5.640000000000001
Les annotations sont pour les humains et les outils, pas pour l’interpréteur. Pour la validation à l’exécution, il faut soit isinstance(), soit des bibliothèques comme Pydantic.
48.2 Syntaxe de base
Variables
age: int = 30
nom: str = "Alice"
pi: float = 3.14
est_actif: bool = True
notes: list = [12, 15, 14]L’annotation vient après le nom, séparée par :.
Fonctions : paramètres et retour
def saluer(nom: str) -> str:
return f"Bonjour {nom}"
def age_est_majeur(age: int) -> bool:
return age >= 18
def afficher(message: str) -> None: # ne renvoie rien
print(message)-> None signifie que la fonction ne renvoie rien (ou None implicite).
Paramètres avec valeur par défaut
def saluer(nom: str, politesse: str = "Bonjour") -> str:
return f"{politesse} {nom}"48.3 Les types de base
Types primitifs
| Type Python | Annotation |
|---|---|
| Entier | int |
| Flottant | float |
| Chaîne | str |
| Booléen | bool |
| Octets | bytes |
| Rien | None |
Collections (avant Python 3.9)
Pour les collections typées, on devait importer depuis typing :
from typing import List, Dict, Tuple, Set
notes: List[int] = [12, 15, 14]
ages: Dict[str, int] = {"Alice": 30}
point: Tuple[float, float] = (3.0, 4.0)
valeurs: Set[str] = {"a", "b", "c"}Collections (Python 3.9+) — syntaxe moderne
Depuis Python 3.9, on peut utiliser directement les types builtin avec des crochets :
notes: list[int] = [12, 15, 14]
ages: dict[str, int] = {"Alice": 30, "Bob": 25}
point: tuple[float, float] = (3.0, 4.0)
valeurs: set[str] = {"a", "b", "c"}
# Plus besoin d'importer !Adoptez cette syntaxe — c’est la norme actuelle.
- Liste :
list[int]— taille variable, tous du même type. - Tuple fixe :
tuple[int, str, float]— 3 éléments, types précis. - Tuple variable :
tuple[int, ...]— nombre variable d’int.
coordonnee: tuple[float, float] = (3.0, 4.0) # exactement 2 float
chemin: tuple[int, ...] = (1, 2, 3, 4, 5) # nombre variable d'int48.4 Types optionnels et unions
Optional : peut être None
Souvent, une valeur peut être None. On l’exprime avec Optional ou | None (Python 3.10+).
# Ancienne syntaxe
from typing import Optional
def chercher(nom: str) -> Optional[str]:
# peut renvoyer str OU None
if nom == "Alice":
return "trouvé"
return None
# Python 3.10+ : syntaxe avec |
def chercher_moderne(nom: str) -> str | None:
if nom == "Alice":
return "trouvé"
return NoneLes deux sont équivalents. Préférez str | None pour du code moderne.
Union : plusieurs types possibles
# Ancienne syntaxe
from typing import Union
def convertir(valeur: Union[int, str]) -> int:
return int(valeur)
# Python 3.10+
def convertir_moderne(valeur: int | str) -> int:
return int(valeur)
print(convertir_moderne(42))
print(convertir_moderne("100"))42
100
La syntaxe | est lisible et identique à celle qu’on utilise en pattern matching.
48.5 Any : échappatoire
Any désactive la vérification de type. À utiliser avec parcimonie — signifie « je ne sais pas / tout va ».
from typing import Any
def stocker_n_importe_quoi(x: Any) -> None:
"""Accepte n'importe quel type."""
print(f"Stocké : {x} (type {type(x).__name__})")
stocker_n_importe_quoi(42)
stocker_n_importe_quoi("Hello")
stocker_n_importe_quoi([1, 2, 3])Stocké : 42 (type int)
Stocké : Hello (type str)
Stocké : [1, 2, 3] (type list)
Any trahit l’intérêt du typage — ne l’utilisez que pour du code qui accepte vraiment tout type (décorateurs génériques, APIs très flexibles).
48.6 Alias de types
Pour éviter de répéter une annotation complexe, on peut créer un alias.
# Ancienne syntaxe (Python < 3.12)
from typing import Dict, List
Vecteur = list[float]
Matrice = list[list[float]]
Base = dict[str, int]
def produit_scalaire(v1: Vecteur, v2: Vecteur) -> float:
return sum(a * b for a, b in zip(v1, v2))
print(produit_scalaire([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]))32.0
type keyword
Python 3.12 a introduit un mot-clé type pour définir des alias explicitement :
type Vecteur = list[float] # Python 3.12+Plus propre et plus explicite. Pour le TOSA (Python 3.10), utilisez la forme simple Vecteur = list[float].
48.7 Callable : fonctions en paramètre
Pour annoter un paramètre qui est une fonction, on utilise Callable.
from typing import Callable
def appliquer(fonction: Callable[[int], int], valeur: int) -> int:
"""Applique `fonction` à `valeur`."""
return fonction(valeur)
def double(x: int) -> int:
return x * 2
print(appliquer(double, 5))
print(appliquer(lambda x: x + 100, 5))10
105
Syntaxe : Callable[[types_arguments], type_retour].
Callable[[int], int]: fonction qui prend unintet renvoie unint.Callable[[str, int], None]: prend(str, int), ne renvoie rien.Callable[..., bool]: n’importe quels arguments, renvoiebool.
48.8 Generic types avec TypeVar
Pour des fonctions qui acceptent n’importe quel type mais préservent ce type, on utilise TypeVar.
from typing import TypeVar
T = TypeVar("T")
def premier(liste: list[T]) -> T:
"""Renvoie le premier élément, du même type que la liste."""
return liste[0]
# Le type du retour suit le type de la liste
x: int = premier([1, 2, 3]) # x est bien int
y: str = premier(["a", "b", "c"]) # y est bien str
print(x, y)1 a
Sans TypeVar, on aurait utilisé Any — perdant l’info de type. Avec, l’outil sait que premier([1, 2]) renvoie un int.
48.9 Annotations de classes
Attributs d’instance
class Personne:
nom: str
age: int
email: str | None = None
def __init__(self, nom: str, age: int) -> None:
self.nom = nom
self.age = ageMéthodes
class CompteBancaire:
def __init__(self, titulaire: str, solde: float = 0.0) -> None:
self.titulaire = titulaire
self.solde = solde
def deposer(self, montant: float) -> None:
if montant <= 0:
raise ValueError("Montant invalide")
self.solde += montant
def consulter(self) -> float:
return self.soldeAuto-référence : le type de la classe elle-même
Parfois une méthode renvoie une instance de sa propre classe — souci : la classe n’est pas encore définie au moment où on l’annote.
Python 3.11+ : Self (depuis typing).
from typing import Self
class Compte:
def doubler(self) -> Self:
nouveau = Compte()
nouveau.solde = self.solde * 2
return nouveauAvant 3.11 : chaîne de caractères (forward reference).
class Compte:
def __init__(self, solde: float = 0) -> None:
self.solde = solde
def doubler(self) -> "Compte": # ← chaîne
nouveau = Compte()
nouveau.solde = self.solde * 2
return nouveau
c = Compte(100)
print(c.doubler().solde)200
48.10 mypy — la vérification statique
mypy analyse votre code et détecte les erreurs de type sans l’exécuter.
Installation
pip install mypyUtilisation
# Vérifier un fichier
mypy mon_script.py
# Vérifier un dossier
mypy src/Exemple
# fichier : calcul.py
def additionner(a: int, b: int) -> int:
return a + b
# Usage incorrect
resultat = additionner("Hello", "World")$ mypy calcul.py
calcul.py:5: error: Argument 1 to "additionner" has incompatible type "str"; expected "int"
calcul.py:5: error: Argument 2 to "additionner" has incompatible type "str"; expected "int"mypy repère le bug avant que l’utilisateur ne lance le programme.
Options utiles
mypy --strict # mode le plus strict (recommandé pour projets pros)
mypy --ignore-missing-imports # ignore les modules non annotés
mypy --show-error-codes # affiche le code de chaque erreurLe mode --strict active toutes les vérifications :
- Tous les arguments et retours doivent être annotés.
- Pas d’
Anyimplicite. - Les fonctions sans annotation sont signalées.
48.11 Configurer mypy
Dans un fichier mypy.ini ou pyproject.toml :
# mypy.ini
[mypy]
python_version = 3.10
strict = True
warn_unused_ignores = True
disallow_untyped_defs = TrueOu dans pyproject.toml :
[tool.mypy]
python_version = "3.10"
strict = true48.12 Types avancés pour le TOSA Expert
Literal — valeur constante
Pour restreindre à certaines valeurs précises :
from typing import Literal
def set_niveau(niveau: Literal["debug", "info", "warning", "error"]) -> None:
print(f"Niveau : {niveau}")
set_niveau("info") # OK
# set_niveau("verbose") # mypy : erreur !Niveau : info
TypedDict — dicts typés
Pour un dict avec des clés connues et typées :
from typing import TypedDict
class Utilisateur(TypedDict):
nom: str
age: int
admin: bool
# Usage : comme un dict normal, mais mypy vérifie
alice: Utilisateur = {"nom": "Alice", "age": 30, "admin": True}
print(alice["nom"])Alice
TypedDict ne change pas le comportement à l’exécution, mais mypy vérifie la structure.
NamedTuple — tuples avec noms typés
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
p = Point(3.0, 4.0)
print(p.x, p.y) # accès par nom
print(p[0], p[1]) # accès par indice (reste un tuple)3.0 4.0
3.0 4.0
Équivalent léger et immuable d’un @dataclass.
Protocol — typage structurel (Python 3.8+)
Protocol permet de typer par ce qu’un objet peut faire, pas par sa hiérarchie de classes. C’est le « duck typing » statique.
from typing import Protocol
class AvecNom(Protocol):
nom: str
def afficher_nom(obj: AvecNom) -> None:
print(obj.nom)
# N'importe quelle classe avec un attribut .nom de type str fait l'affaire
class Personne:
def __init__(self, nom: str) -> None:
self.nom = nom
class Produit:
def __init__(self, nom: str) -> None:
self.nom = nom
afficher_nom(Personne("Alice"))
afficher_nom(Produit("Pain"))Alice
Pain
Pas besoin d’héritage. mypy vérifie juste que l’objet a un nom: str.
48.13 Quand annoter (et quand pas)
Annotez systématiquement :
- Les signatures publiques : arguments et retours de fonctions/méthodes exposées.
- Les attributs de classe qui ne sont pas initialisés avec une valeur typée.
Pas besoin d’annoter :
- Les variables locales évidentes (
x = 10,nom = "Alice"). - Du code de script jetable ou prototype.
Cas délicat :
- Les
*argset**kwargs:*args: int,**kwargs: str. - Les fonctions qui renvoient des types complexes : prenez le temps, ça documente.
48.14 Exemple complet
from dataclasses import dataclass
@dataclass
class Produit:
nom: str
prix: float
quantite: int = 1
def valeur_totale(self) -> float:
return self.prix * self.quantite
def panier_total(produits: list[Produit], tva: float = 0.20) -> float:
"""Calcule le total TTC d'un panier."""
total_ht = sum(p.valeur_totale() for p in produits)
return round(total_ht * (1 + tva), 2)
def produit_le_plus_cher(produits: list[Produit]) -> Produit | None:
"""Renvoie le produit le plus cher, ou None si panier vide."""
if not produits:
return None
return max(produits, key=lambda p: p.prix)
# Utilisation
panier: list[Produit] = [
Produit("Pain", 1.20, 2),
Produit("Lait", 0.95, 3),
Produit("Fromage", 4.50),
]
print(f"Total TTC : {panier_total(panier):.2f} €")
print(f"Plus cher : {produit_le_plus_cher(panier)}")Total TTC : 11.70 €
Plus cher : Produit(nom='Fromage', prix=4.5, quantite=1)
Ce code est entièrement typé. mypy peut le vérifier sans signaler d’erreur. Un nouvel arrivant dans l’équipe comprend les signatures sans lire les corps.
🧩 Quiz 8.1 — Type hints
Question 1
Python vérifie-t-il les annotations à l’exécution ?
- Oui, toujours
- Non — elles sont pour les outils et la doc uniquement
- Seulement si on l’active
- Oui, mais seulement en mode debug
b) Non — Python ignore les annotations au runtime. Pour vérifier, on utilise mypy (statique) ou des bibliothèques comme Pydantic (runtime).
Question 2
Quelle annotation utiliser pour une fonction qui ne renvoie rien ?
-> void-> null-> None- Rien
c) -> None — c’est explicite et détectable par mypy. Omettre l’annotation est aussi légal mais moins informatif.
Question 3
Comment annote-t-on « liste d’entiers » en Python 3.10 ?
List[int]list[int]list<int>[int]
b) list[int] — syntaxe moderne (Python 3.9+) avec les builtins. List[int] depuis typing fonctionne encore mais est déprécié.
Question 4
Quelle est l’équivalent moderne de Optional[str] en Python 3.10 ?
str | Nonestr | NullMaybe[str]str?
a) str | None — Optional[X] est strictement équivalent à X | None. La forme | est préférée en Python 3.10+.
Question 5
À quoi sert Any ?
- À indiquer qu’on accepte tout type (désactive la vérif)
- À indiquer un booléen
- À indiquer une erreur
- À signaler une variable obligatoire
a) — Any est un échappatoire au typage. À utiliser avec parcimonie : trop d’Any équivaut à ne pas typer du tout.
Question 6
Quelle syntaxe annote « fonction prenant un int et renvoyant un str » ?
Function[int, str]Callable[[int], str]int -> strCallable(int, str)
b) Callable[[int], str] — arguments dans une liste, type de retour après.
from typing import Callable
def appliquer(f: Callable[[int], str], x: int) -> str:
return f(x)Question 7
Que fait mypy ?
- Exécute le code avec vérification de types
- Analyse le code statiquement pour détecter les erreurs de type avant exécution
- Formate le code
- Ajoute automatiquement des annotations
b) — analyse statique (sans exécution). Détecte les incohérences de type à la lecture du code.
Question 8
Pour annoter un dict {"nom": "Alice", "age": 30} avec structure fixe :
dict[str, str]dict[str, Any]- Un
TypedDict NamedTuple
c) TypedDict — permet de typer chaque clé différemment (nom: str, age: int). dict[str, Any] perd l’info. Un @dataclass est souvent encore mieux selon le cas.
✏️ Exercice 8.1 — Annoter une fonction simple
Ajoutez les annotations à cette fonction.
def moyenne(notes):
if not notes:
return None
return sum(notes) / len(notes)def moyenne(notes: list[float]) -> float | None:
if not notes:
return None
return sum(notes) / len(notes)
print(moyenne([12, 15, 14]))
print(moyenne([]))13.666666666666666
None
Alternatives valables
# Accepter aussi des int (les int sont compatibles avec float)
def moyenne(notes: list[float]) -> float | None: ...
# Plus strict : seulement int
def moyenne(notes: list[int]) -> float | None: ...
# Accepter n'importe quel nombre
from typing import Union
def moyenne(notes: list[int | float]) -> float | None: ...En pratique : list[float] accepte list[int] pour mypy (covariance numérique).
✏️ Exercice 8.2 — Typer une classe
Annotez entièrement cette classe.
class CompteBancaire:
def __init__(self, titulaire, solde=0):
self.titulaire = titulaire
self.solde = solde
self.historique = []
def deposer(self, montant):
if montant <= 0:
raise ValueError("Montant invalide")
self.solde += montant
self.historique.append(("depot", montant))
def resume(self):
return {
"titulaire": self.titulaire,
"solde": self.solde,
"operations": len(self.historique),
}class CompteBancaire:
titulaire: str
solde: float
historique: list[tuple[str, float]]
def __init__(self, titulaire: str, solde: float = 0) -> None:
self.titulaire = titulaire
self.solde = solde
self.historique = []
def deposer(self, montant: float) -> None:
if montant <= 0:
raise ValueError("Montant invalide")
self.solde += montant
self.historique.append(("depot", montant))
def resume(self) -> dict[str, str | float | int]:
return {
"titulaire": self.titulaire,
"solde": self.solde,
"operations": len(self.historique),
}
# Test
c = CompteBancaire("Alice", 100)
c.deposer(50)
print(c.resume()){'titulaire': 'Alice', 'solde': 150, 'operations': 1}
Commentaires
list[tuple[str, float]]: liste de tuples(type_operation, montant).dict[str, str | float | int]: les valeurs sont hétérogènes. Dans un vrai projet, unTypedDictserait plus précis :
from typing import TypedDict
class ResumeCompte(TypedDict):
titulaire: str
solde: float
operations: int
class CompteBancaire:
def __init__(self, titulaire: str, solde: float = 0) -> None:
self.titulaire = titulaire
self.solde = solde
self.historique: list[tuple[str, float]] = []
def resume(self) -> ResumeCompte:
return {
"titulaire": self.titulaire,
"solde": self.solde,
"operations": len(self.historique),
}
c = CompteBancaire("Alice", 100)
print(c.resume()){'titulaire': 'Alice', 'solde': 100, 'operations': 0}
✏️ Exercice 8.3 — Fonction générique avec TypeVar
Écrivez une fonction générique dernier(sequence) qui renvoie le dernier élément d’une séquence, du même type que les éléments.
from typing import TypeVar
T = TypeVar("T")
def dernier(sequence: ...) -> ...:
...
# Tests (mypy doit savoir le type retourné)
x: int = dernier([1, 2, 3]) # 3
y: str = dernier(["a", "b", "c"]) # 'c'from typing import TypeVar
T = TypeVar("T")
def dernier(sequence: list[T]) -> T:
if not sequence:
raise ValueError("séquence vide")
return sequence[-1]
# Tests
x: int = dernier([1, 2, 3])
print(x, type(x))
y: str = dernier(["a", "b", "c"])
print(y, type(y))
z: float = dernier([1.5, 2.5, 3.5])
print(z, type(z))3 <class 'int'>
c <class 'str'>
3.5 <class 'float'>
Bonus : fonctionne aussi sur les tuples
Avec une protocole Sequence plus général :
from typing import TypeVar
from collections.abc import Sequence
T = TypeVar("T")
def dernier(sequence: Sequence[T]) -> T:
if not sequence:
raise ValueError("séquence vide")
return sequence[-1]
print(dernier([1, 2, 3]))
print(dernier((10, 20, 30)))
print(dernier("hello"))3
30
o
Sequence accepte list, tuple, str — n’importe quel objet indexable de taille finie.
✏️ Exercice 8.4 — Détecter les erreurs de type
Pour chaque extrait, identifiez l’erreur que mypy signalerait.
# A
def doubler(x: int) -> int:
return x * 2
doubler("hello")
# B
def premier(liste: list[int]) -> int:
return liste[0]
premier([]) # quelle erreur pourrait remonter à l'exécution, mais mypy ?
# C
def saluer(nom: str | None) -> str:
return f"Bonjour {nom.upper()}"
# D
from typing import Any
def charger_config(path: str) -> Any:
return {} # renvoie un dict, mais annoté Any
data = charger_config("...")
print(data.xyz) # erreur ou pas pour mypy ?A — Erreur : doubler("hello") passe un str alors qu’on attend int. mypy signale une incompatibilité de type.
B — Pas d’erreur mypy : l’appel premier([]) respecte la signature (list[int]). Mais à l’exécution, liste[0] lève IndexError. mypy ne vérifie pas la longueur des séquences.
Ce cas illustre : mypy ne garantit pas l’absence de bugs runtime, seulement la cohérence de types.
C — Erreur mypy : nom peut être None, sur lequel .upper() ne fonctionne pas. Correction :
def saluer(nom: str | None) -> str:
if nom is None:
return "Bonjour inconnu"
return f"Bonjour {nom.upper()}"
print(saluer("Alice"))
print(saluer(None))Bonjour ALICE
Bonjour inconnu
Une fois le if nom is None fait, mypy comprend (par narrowing) qu’en dessous nom est forcément str.
D — Pas d’erreur mypy : Any désactive la vérification. data.xyz passe — Any autorise tout.
Leçon : abuser d’Any fait perdre les garanties du typage.
À retenir
- Type hints = annotations de type :
x: int,def f() -> bool:. Python les ignore au runtime. - Syntaxe moderne (3.9+) :
list[int],dict[str, int],tuple[float, float]. - Union :
str | int(Python 3.10+), optionnel :str | None. Callable[[int], str]pour typer une fonction en paramètre.TypeVarpour des fonctions génériques qui préservent le type.Literal["a", "b"],TypedDict,NamedTuple,Protocolpour des cas avancés.Anydésactive la vérif : à utiliser avec parcimonie.mypyvérifie statiquement les types. Runmypy monscript.py.- Auto-référence :
"MaClasse"ouSelf(3.11+). - Annotez les signatures publiques, pas forcément chaque variable locale.