17. Typage Statique Avancé
TypeVar,Generic,Protocol,@overload,Self,TypeGuard,Final,Literal,TypedDict,Never, covariance/contravariance.
17.1 TypeVar — variables de type
from typing import TypeVar
T = TypeVar("T") # n'importe quel type
U = TypeVar("U", int, str) # contraint : int ou str
V = TypeVar("V", bound=Number) # doit être ou hériter de Numberdef premier(seq: list[T]) -> T:
return seq[0]
premier([1, 2, 3]) # int
premier(["a", "b"]) # strCovariance, contravariance, invariance
T_co = TypeVar("T_co", covariant=True) # list[Cat] est sous-type de list[Animal]
T_contra = TypeVar("T_contra", contravariant=True)- invariant (défaut) :
list[Dog]n’est paslist[Animal] - covariant : producteur de
T(ex:Iterator[T_co]) - contravariant : consommateur de
T(ex:Callable[[T_contra], None])
17.2 Generic — classes génériques
from typing import Generic, TypeVar
T = TypeVar("T")
class Pile(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
# Usage
pile_int = Pile[int]()
pile_str = Pile[str]()Héritage de génériques
class PileOrdonnée(Pile[T]):
def push(self, item: T) -> None:
...Generic avec contraintes
from typing import Generic, TypeVar
Num = TypeVar("Num", int, float)
class Vecteur(Generic[Num]):
def __init__(self, x: Num, y: Num):
self.x = x
self.y = y17.3 Protocol — duck typing statique (PEP 544)
Un Protocol définit une interface sans héritage :
from typing import Protocol
class Volant(Protocol):
def voler(self) -> None: ...
class Oiseau:
def voler(self) -> None:
print("L'oiseau vole")
class Avion:
def voler(self) -> None:
print("L'avion vole")
def faire_voler(obj: Volant) -> None:
obj.voler()
faire_voler(Oiseau()) # OK
faire_voler(Avion()) # OKComparaison avec l’héritage formel :
# Approche traditionnelle (héritage)
class Volant(ABC):
@abstractmethod
def voler(self) -> None: ...
# Approche Protocol (duck typing)
class Volant(Protocol):
def voler(self) -> None: ...Protocol avec TypeVar
from typing import Protocol, TypeVar
T = TypeVar("T", bound="Comparable")
class Comparable(Protocol):
def __lt__(self, other: object) -> bool: ...
def max_deux(a: T, b: T) -> T:
return a if a > b else b@runtime_checkable
Vérifiable à l’exécution avec isinstance :
from typing import Protocol, runtime_checkable
@runtime_checkable
class Iterable(Protocol):
def __iter__(self): ...
isinstance([1, 2, 3], Iterable) # True17.4 @overload — signatures multiples
Déclarer plusieurs signatures pour une même fonction :
from typing import overload
@overload
def doublé(x: int) -> int: ...
@overload
def doublé(x: str) -> str: ...
@overload
def doublé(x: list[T]) -> list[T]: ...
def doublé(x: int | str | list[T]) -> int | str | list[T]:
if isinstance(x, int):
return x * 2
if isinstance(x, str):
return x * 2
return [item * 2 for item in x]Usage typique : numpy.ndarray.__getitem__ a des douzaines de @overload.
17.5 Self (3.11+)
Retourner le type de self dans une classe :
from typing import Self
class Builder:
def set_nom(self, nom: str) -> Self:
self.nom = nom
return self
def set_âge(self, âge: int) -> Self:
self.âge = âge
return self
class BuilderEnfant(Builder):
def set_école(self, école: str) -> Self:
self.école = école
return self
# Le type retourné est BuilderEnfant, pas Builder
b = BuilderEnfant().set_nom("Alice").set_école("Lycée")Avant 3.11, il fallait écrire :
from typing import TypeVar
T = TypeVar("T", bound="Builder")
class Builder:
def set_nom(self: T, nom: str) -> T:
...17.6 TypeGuard — narrowing par fonction (3.10+)
from typing import TypeGuard
def est_str_liste(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
def traiter(val: list[object]) -> None:
if est_str_liste(val):
# ici mypy sait que val est list[str]
print(" ".join(val))17.7 Final — constantes et méthodes non surchargeables
from typing import Final
PI: Final = 3.14159
# PI = 3 # mypy: error
class Base:
def méthode_finale(self) -> None: ...17.8 Literal — valeurs littérales exactes
from typing import Literal
def set_mode(mode: Literal["train", "eval", "predict"]) -> None:
...
set_mode("train") # OK
set_mode("unknown") # mypy: error
def open_file(mode: Literal["r", "w", "a", "rb"]) -> None: ...
type Mode = Literal["train", "eval"]
def basculer(mode: Mode) -> None: ...Literal vs Enum
from typing import Literal
from enum import Enum
# Approche Literal — typage statique uniquement
ModeStr = Literal["train", "eval", "predict"]
def basculer(mode: ModeStr) -> None: ...
basculer("train") # OK
basculer("unknown") # mypy/pyright: error
# Approche Enum — existe à l'exécution
class Mode(Enum):
TRAIN = "train"
EVAL = "eval"
PREDICT = "predict"
def basculer(mode: Mode) -> None: ...
basculer(Mode.TRAIN) # OK
basculer("train") # mypy: error (attendu Mode, pas str)Quand utiliser quoi ?
| Critère | Literal | Enum |
|---|---|---|
| Existence runtime | Disparaît à la compilation | Classe réelle, isinstance(m, Mode) |
| Valeurs autorisées | Chaînes/nombres/booleans uniquement | N’importe quoi (StrEnum, IntEnum, etc.) |
| Autocomplétion IDE | Limitée (suggestions de base) | Excellente (Mode.TRAIN suggéré) |
| Sérialisation | Directe ("train" s’écrit tel quel) | Besoin de .value ou d’un helper |
| Pattern matching | match x: case "train": | match x: case Mode.TRAIN: (3.10+) |
| Itération | Impossible | list(Mode) fonctionne |
| Ajout d’attributs | Impossible | Méthodes, @property, auto() |
| Aliasing | Possible via type Mode = Literal[...] | Mode.TRAIN is Mode.TRAIN (singleton) |
Recommandation :
Enum: valeur connue et réutilisée dans tout le codebase, besoin de méthodes, itération, validation runtime, sérialisation personnaliséeLiteral: valeur passagère, paramètre de fonction rare, compatibilité avec des APIs externes qui attendent desstr, typage rapide sans créer de classe
# Bon cas pour Literal : paramètre rare
def set_temp(temp: float, unité: Literal["C", "F", "K"]) -> None: ...
# Bon cas pour Enum : concept central du domaine
class Status(IntEnum):
OK = 200
NOT_FOUND = 404
ERROR = 50017.9 Never — fonction qui ne retourne jamais
from typing import Never
def erreur(msg: str) -> Never:
raise RuntimeError(msg)
def boucle_infinie() -> Never:
while True:
pass17.10 Annotated — métadonnées sur les types (3.11+)
from typing import Annotated
def set_temp(temp: Annotated[float, "Celsius"]) -> None: ...
def set_age(age: Annotated[int, GTE(0), LTE(150)]) -> None: ...17.11 TypeAlias (3.10+)
from typing import TypeAlias
Vector: TypeAlias = list[float]
Matrice: TypeAlias = list[list[float]]
def produit_scalaire(a: Vector, b: Vector) -> float: ...17.12 Unpack — dépaqueter des *args typés
from typing import Unpack
class Position(TypedDict):
x: int
y: int
def déplacer(**kwargs: Unpack[Position]) -> None: ...
déplacer(x=10, y=20)17.13 Configuration mypy
# mypy.ini
[mypy]
python_version = 3.13
strict = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_any_unimported = Truemypy mon_script.py # Vérification
mypy --strict mon_script.py # Mode strict17.14 Bonnes pratiques
- Utiliser
TypeVaravecboundplutôt queAny - Toujours annoter les fonctions publiques et les signatures
Protocolpour les interfaces implicites (duck typing typé)@overloadpour les fonctions à comportement différent selon le typeFinalpour les vraies constantes- Utiliser
Selfpour les builders etcopy()/replace() - Éviter
Anyautant que possible