Sécurité des Types en Python
Exploitez le système de types de Python pour capturer les erreurs lors de l'analyse statique. Les annotations de type servent de documentation appliquée que les outils valident automatiquement.
Quand Utiliser Cette Compétence
- Ajouter des indications de type au code existant
- Créer des classes génériques et réutilisables
- Définir des interfaces structurelles avec des protocoles
- Configurer mypy ou pyright pour une vérification stricte
- Comprendre le rétrécissement de type et les gardes
- Construire des APIs et des bibliothèques type-safe
Concepts Fondamentaux
1. Annotations de Type
Déclarez les types attendus pour les paramètres de fonction, les valeurs de retour et les variables.
2. Génériques
Écrivez du code réutilisable qui préserve les informations de type sur différents types.
3. Protocoles
Définissez des interfaces structurelles sans héritage (duck typing avec sécurité de type).
4. Rétrécissement de Type
Utilisez des gardes et des conditions pour réduire les types dans les blocs de code.
Démarrage Rapide
def get_user(user_id: str) -> User | None:
"""Le type de retour rend explicite « pourrait ne pas exister »."""
...
# Le vérificateur de type force la gestion du cas None
user = get_user("123")
if user is None:
raise UserNotFoundError("123")
print(user.name) # Le vérificateur de type sait que user est User ici
Modèles Fondamentaux
Modèle 1 : Annoter Toutes les Signatures Publiques
Chaque fonction publique, méthode et classe doit avoir des annotations de type.
def get_user(user_id: str) -> User:
"""Récupérer l'utilisateur par ID."""
...
def process_batch(
items: list[Item],
max_workers: int = 4,
) -> BatchResult[ProcessedItem]:
"""Traiter les éléments de manière concurrente."""
...
class UserRepository:
def __init__(self, db: Database) -> None:
self._db = db
async def find_by_id(self, user_id: str) -> User | None:
"""Retourner User si trouvé, None sinon."""
...
async def find_by_email(self, email: str) -> User | None:
...
async def save(self, user: User) -> User:
"""Sauvegarder et retourner l'utilisateur avec l'ID généré."""
...
Utilisez mypy --strict ou pyright en CI pour capturer les erreurs de type en amont. Pour les projets existants, activez le mode strict progressivement en utilisant des remplacements par module.
Modèle 2 : Utiliser la Syntaxe Union Moderne
Python 3.10+ offre une syntaxe d'union plus propre.
# Préféré (3.10+)
def find_user(user_id: str) -> User | None:
...
def parse_value(v: str) -> int | float | str:
...
# Style ancien (toujours valide, nécessaire pour 3.9)
from typing import Optional, Union
def find_user(user_id: str) -> Optional[User]:
...
Modèle 3 : Rétrécissement de Type avec Gardes
Utilisez des conditions pour réduire les types pour le vérificateur de type.
def process_user(user_id: str) -> UserData:
user = find_user(user_id)
if user is None:
raise UserNotFoundError(f"User {user_id} not found")
# Le vérificateur de type sait que user est User ici, pas User | None
return UserData(
name=user.name,
email=user.email,
)
def process_items(items: list[Item | None]) -> list[ProcessedItem]:
# Filtrer et réduire les types
valid_items = [item for item in items if item is not None]
# valid_items est maintenant list[Item]
return [process(item) for item in valid_items]
Modèle 4 : Classes Génériques
Créez des conteneurs réutilisables et type-safe.
from typing import TypeVar, Generic
T = TypeVar("T")
E = TypeVar("E", bound=Exception)
class Result(Generic[T, E]):
"""Représente soit une valeur de succès soit une erreur."""
def __init__(
self,
value: T | None = None,
error: E | None = None,
) -> None:
if (value is None) == (error is None):
raise ValueError("Exactly one of value or error must be set")
self._value = value
self._error = error
@property
def is_success(self) -> bool:
return self._error is None
@property
def is_failure(self) -> bool:
return self._error is not None
def unwrap(self) -> T:
"""Obtenir la valeur ou lever l'erreur."""
if self._error is not None:
raise self._error
return self._value # type: ignore[return-value]
def unwrap_or(self, default: T) -> T:
"""Obtenir la valeur ou retourner la valeur par défaut."""
if self._error is not None:
return default
return self._value # type: ignore[return-value]
# L'usage préserve les types
def parse_config(path: str) -> Result[Config, ConfigError]:
try:
return Result(value=Config.from_file(path))
except ConfigError as e:
return Result(error=e)
result = parse_config("config.yaml")
if result.is_success:
config = result.unwrap() # Type : Config
Modèles Avancés
Modèle 5 : Dépôt Générique
Créez des modèles d'accès aux données type-safe.
from typing import TypeVar, Generic
from abc import ABC, abstractmethod
T = TypeVar("T")
ID = TypeVar("ID")
class Repository(ABC, Generic[T, ID]):
"""Interface de dépôt générique."""
@abstractmethod
async def get(self, id: ID) -> T | None:
"""Obtenir l'entité par ID."""
...
@abstractmethod
async def save(self, entity: T) -> T:
"""Sauvegarder et retourner l'entité."""
...
@abstractmethod
async def delete(self, id: ID) -> bool:
"""Supprimer l'entité, retourner True si elle existait."""
...
class UserRepository(Repository[User, str]):
"""Dépôt concret pour les utilisateurs avec des IDs en chaîne."""
async def get(self, id: str) -> User | None:
row = await self._db.fetchrow(
"SELECT * FROM users WHERE id = $1", id
)
return User(**row) if row else None
async def save(self, entity: User) -> User:
...
async def delete(self, id: str) -> bool:
...
Modèle 6 : TypeVar avec Limites
Restreignez les paramètres génériques à des types spécifiques.
from typing import TypeVar
from pydantic import BaseModel
ModelT = TypeVar("ModelT", bound=BaseModel)
def validate_and_create(model_cls: type[ModelT], data: dict) -> ModelT:
"""Créer un modèle Pydantic validé à partir d'un dict."""
return model_cls.model_validate(data)
# Fonctionne avec n'importe quelle sous-classe BaseModel
class User(BaseModel):
name: str
email: str
user = validate_and_create(User, {"name": "Alice", "email": "a@b.com"})
# user est typé en tant que User
# Erreur de type : str n'est pas une sous-classe BaseModel
result = validate_and_create(str, {"name": "Alice"}) # Error!
Modèle 7 : Protocoles pour le Typage Structurel
Définissez des interfaces sans nécessiter l'héritage.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializable(Protocol):
"""N'importe quelle classe qui peut être sérialisée vers/depuis un dict."""
def to_dict(self) -> dict:
...
@classmethod
def from_dict(cls, data: dict) -> "Serializable":
...
# User satisfait Serializable sans en hériter
class User:
def __init__(self, id: str, name: str) -> None:
self.id = id
self.name = name
def to_dict(self) -> dict:
return {"id": self.id, "name": self.name}
@classmethod
def from_dict(cls, data: dict) -> "User":
return cls(id=data["id"], name=data["name"])
def serialize(obj: Serializable) -> str:
"""Fonctionne avec n'importe quel objet Serializable."""
return json.dumps(obj.to_dict())
# Fonctionne - User correspond au protocole
serialize(User("1", "Alice"))
# Vérification à l'exécution avec @runtime_checkable
isinstance(User("1", "Alice"), Serializable) # True
Modèle 8 : Modèles de Protocole Courants
Définissez des interfaces structurelles réutilisables.
from typing import Protocol
class Closeable(Protocol):
"""Ressource qui peut être fermée."""
def close(self) -> None: ...
class AsyncCloseable(Protocol):
"""Ressource asynchrone qui peut être fermée."""
async def close(self) -> None: ...
class Readable(Protocol):
"""Objet à partir duquel on peut lire."""
def read(self, n: int = -1) -> bytes: ...
class HasId(Protocol):
"""Objet avec une propriété ID."""
@property
def id(self) -> str: ...
class Comparable(Protocol):
"""Objet qui supporte la comparaison."""
def __lt__(self, other: "Comparable") -> bool: ...
def __le__(self, other: "Comparable") -> bool: ...
Modèle 9 : Alias de Type
Créez des noms de type significatifs.
Note : La syntaxe de l'instruction type Alias = ... (PEP 695) a été introduite en Python 3.12, non en 3.10. Pour les projets ciblant les versions antérieures (incluant 3.10/3.11), utilisez l'annotation TypeAlias (PEP 613, disponible depuis Python 3.10).
# Instruction type Python 3.12+ (PEP 695)
type UserId = str
type UserDict = dict[str, Any]
# Instruction type Python 3.12+ avec génériques (PEP 695)
type Handler[T] = Callable[[Request], T]
type AsyncHandler[T] = Callable[[Request], Awaitable[T]]
# Style Python 3.10-3.11 (nécessaire pour une meilleure compatibilité)
from typing import TypeAlias
from collections.abc import Callable, Awaitable
UserId: TypeAlias = str
Handler: TypeAlias = Callable[[Request], Response]
# Utilisation
def register_handler(path: str, handler: Handler[Response]) -> None:
...
Modèle 10 : Types Callable
Typez les paramètres de fonction et les callbacks.
from collections.abc import Callable, Awaitable
# Callback synchrone
ProgressCallback = Callable[[int, int], None] # (current, total)
# Callback asynchrone
AsyncHandler = Callable[[Request], Awaitable[Response]]
# Avec paramètres nommés (utilisant Protocol)
class OnProgress(Protocol):
def __call__(
self,
current: int,
total: int,
*,
message: str = "",
) -> None: ...
def process_items(
items: list[Item],
on_progress: ProgressCallback | None = None,
) -> list[Result]:
for i, item in enumerate(items):
if on_progress:
on_progress(i, len(items))
...
Configuration
Liste de Vérification du Mode Strict
Pour la conformité mypy --strict :
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
no_implicit_optional = true
Objectifs d'adoption progressive :
- Tous les paramètres de fonction annotés
- Tous les types de retour annotés
- Attributs de classe annotés
- Minimiser l'utilisation de
Any(acceptable pour les données véritablement dynamiques) - Les collections génériques utilisent des paramètres de type (
list[str]paslist)
Pour les bases de code existantes, activez le mode strict par module en utilisant # mypy: strict ou configurez des remplacements par module dans pyproject.toml.
Résumé des Bonnes Pratiques
- Annoter toutes les APIs publiques - Fonctions, méthodes, attributs de classe
- Utiliser
T | None- Syntaxe d'union moderne plutôt queOptional[T] - Exécuter une vérification stricte de type -
mypy --stricten CI - Utiliser les génériques - Préserver les informations de type dans le code réutilisable
- Définir des protocoles - Typage structurel pour les interfaces
- Réduire les types - Utiliser des gardes pour aider le vérificateur de type
- Lier les variables de type - Restreindre les génériques à des types significatifs
- Créer des alias de type - Noms significatifs pour les types complexes
- Minimiser
Any- Utiliser des types spécifiques ou des génériques.Anyest acceptable pour les données véritablement dynamiques ou lors de l'interfaçage avec du code tiers non typé - Documenter avec des types - Les types sont une documentation applicable