Les indices de type (Type Hints)¶
Introduction : Pourquoi les Type Hints ?¶
Dans nos exemples précédents avec @property, nous avons utilisé beaucoup de vérifications isinstance() pour valider
les types. Par exemple :
@nom.setter
def nom(self, valeur):
if not isinstance(valeur, str): # Vérification manuelle du type
raise TypeError("Le nom doit être une chaîne de caractères")
# ...
Ce code a plusieurs problèmes :
- Redondance : Nous devons écrire la même vérification à plusieurs endroits
- Maintenabilité : Si nous changeons le type attendu, nous devons mettre à jour toutes les vérifications
- Documentation : La signature de la méthode ne montre pas clairement quels types sont attendus
- Outils : Les IDE et outils d’analyse statique (comme mypy) ne peuvent pas aider
Les Type Hints (indices de type) résolvent ces problèmes en :
- Documentant les types attendus
- Permettant une vérification automatique
- Améliorant l’expérience développeur avec l’autocomplétion et la détection d’erreurs
Principes de base des Type Hints¶
1. Syntaxe de base¶
Les type hints sont ajoutés après le deux-points dans les définitions de fonctions/méthodes :
Pour les attributs de classe, on utilise des commentaires ou des annotations :
class MaClasse:
attribut: type # Annotation pour les attributs
def __init__(self):
self.attribut = valeur # Pas de vérification automatique ici
2. Types de base¶
Python propose plusieurs types intégrés pour les hints :
def exemple(
entier: int,
flottant: float,
chaine: str,
booleen: bool,
liste: list[int], # Liste d'entiers
tuple: tuple[str, int], # Tuple avec une chaîne et un entier
ensemble: set[float],
dictionnaire: dict[str, int] # Clés chaînes, valeurs entiers
) -> None:
pass
3. Types personnalisés¶
Pour les classes que vous définissez :
class Personne:
def __init__(self, nom: str, age: int):
self.nom = nom
self.age = age
def saluer(personne: Personne) -> str:
return f"Bonjour {personne.nom}"
4. Types optionnels et unions¶
Pour indiquer qu’une valeur peut être de plusieurs types :
from typing import Optional, Union
def exemple(
valeur: Optional[int] = None, # Peut être int ou None
union: Union[str, float] # Peut être str ou float
) -> None:
pass
En Python 3.10+, on peut utiliser | à la place de Union :
Exemple motivant : Classe Personne avec Type Hints¶
Reprenons notre classe Personne et voyons comment les type hints améliorent la situation :
Version sans type hints (avec isinstance)¶
class Personne:
def __init__(self, nom: str, age: int):
if not isinstance(nom, str):
raise TypeError("Le nom doit être une chaîne de caractères")
if not isinstance(age, int) or age < 0:
raise ValueError("L'âge doit être un entier positif")
self._nom = nom.strip()
self._age = age
@property
def nom(self) -> str:
return self._nom
@nom.setter
def nom(self, valeur: str) -> None:
if not isinstance(valeur, str):
raise TypeError("Le nom doit être une chaîne de caractères")
valeur = valeur.strip()
if len(valeur) == 0:
raise ValueError("Le nom ne peut pas être vide après suppression des espaces")
self._nom = valeur
@property
def age(self) -> int:
return self._age
@age.setter
def age(self, valeur: int) -> None:
if not isinstance(valeur, int) or valeur < 0:
raise ValueError("L'âge doit être un entier positif")
self._age = valeur
Version avec type hints (et vérification automatique)¶
from typing import final
class Personne:
def __init__(self, nom: str, age: int):
# Les vérifications de type sont maintenant implicites grâce aux hints
self.nom = nom # Utilise le setter pour valider
self.age = age
@property
def nom(self) -> str:
"""Retourne le nom de la personne."""
return self._nom
@nom.setter
def nom(self, valeur: str) -> None:
"""Définie le nom avec validation."""
valeur = valeur.strip()
if len(valeur) == 0:
raise ValueError("Le nom ne peut pas être vide après suppression des espaces")
self._nom = valeur
@property
def age(self) -> int:
"""Retourne l'âge de la personne."""
return self._age
@age.setter
def age(self, valeur: int) -> None:
"""Définie l'âge avec validation."""
if valeur < 0:
raise ValueError("L'âge doit être positif")
self._age = valeur
# Utilisation avec vérification statique
p = Personne(nom="Alice", age=30)
print(p.nom) # Alice
# Ces lignes seraient détectées par mypy comme des erreurs de type
# p = Personne(nom=123, age="trente") # Erreur: argument 1 doit être str, reçu int
# p.age = "quarante" # Erreur: ne peut pas assigner str à int
Avantages de cette approche :¶
- Documentation claire : La signature montre immédiatement quels types sont attendus
- Vérification automatique : mypy peut détecter les erreurs avant l’exécution
- Moins de code dupliqué : Plus besoin de vérifier
isinstancepour chaque paramètre - Meilleure expérience développeur :
- Autocomplétion dans les IDE
- Détection des erreurs en temps réel
- Meilleure compréhension du code
Exemple complet avec Type Hints et vérification¶
Pour une solution complète, nous pouvons combiner type hints avec une vérification réelle à l’exécution :
from typing import Any, get_type_hints
class Personne:
def __init__(self, nom: str, age: int):
self._valider_types(nom=nom, age=age)
self.nom = nom
self.age = age
@property
def nom(self) -> str:
return self._nom
@nom.setter
def nom(self, valeur: str) -> None:
self._valider_type("nom", valeur)
valeur = valeur.strip()
if len(valeur) == 0:
raise ValueError("Le nom ne peut pas être vide après suppression des espaces")
self._nom = valeur
@property
def age(self) -> int:
return self._age
@age.setter
def age(self, valeur: int) -> None:
self._valider_type("age", valeur)
if valeur < 0:
raise ValueError("L'âge doit être positif")
self._age = valeur
def _valider_types(self, **kwargs: Any) -> None:
"""Valide les types des arguments en utilisant les type hints."""
hints = get_type_hints(self.__init__)
for param, value in kwargs.items():
if param in hints:
expected_type = hints[param]
if not isinstance(value, expected_type):
raise TypeError(
f"Argument '{param}' doit être {expected_type}, "
f"reçu {type(value)}"
)
def _valider_type(self, attr: str, value: Any) -> None:
"""Valide le type d'un attribut en utilisant les type hints."""
hints = get_type_hints(self)
if attr in hints:
expected_type = hints[attr]
if not isinstance(value, expected_type):
raise TypeError(
f"Attribut '{attr}' doit être {expected_type}, "
f"reçu {type(value)}"
)
# Utilisation
try:
p = Personne(nom="Bob", age=25)
print(p.nom) # Bob
# Ces lignes lèveront des exceptions à l'exécution
p.age = "trente" # TypeError: Attribut 'age' doit être <class 'int'>
except (TypeError, ValueError) as e:
print(f"Erreur: {e}")
Exemple avec la classe Point¶
Voici comment nous pourrions réécrire la classe Point avec des type hints :
from typing import Union
class Point:
def __init__(self, x: float = 0.0, y: float = 0.0):
self.x = x
self.y = y
@property
def x(self) -> float:
"""Retourne la coordonnée x du point."""
return self._x
@x.setter
def x(self, valeur: float) -> None:
"""Définie la coordonnée x avec validation."""
if valeur < 0:
raise ValueError("La coordonnée x doit être positive ou nulle")
self._x = float(valeur)
@property
def y(self) -> float:
"""Retourne la coordonnée y du point."""
return self._y
@y.setter
def y(self, valeur: float) -> None:
"""Définie la coordonnée y avec validation."""
if valeur < 0:
raise ValueError("La coordonnée y doit être positive ou nulle")
self._y = float(valeur)
def distance(self, autre_point: 'Point') -> float:
"""Calcule la distance euclidienne entre deux points.
Args:
autre_point: Un autre objet Point
Returns:
La distance entre les deux points
"""
return ((self.x - autre_point.x) ** 2 + (self.y - autre_point.y) ** 2) ** 0.5
def deplacer(self, dx: float, dy: float) -> None:
"""Déplace le point des coordonnées (dx, dy)."""
nouvelle_x = self.x + dx
nouvelle_y = self.y + dy
if nouvelle_x < 0 or nouvelle_y < 0:
raise ValueError("Le déplacement résultant en des coordonnées négatives")
self.x = nouvelle_x
self.y = nouvelle_y
# Utilisation avec vérification statique
p1: Point = Point(3.0, 4.0)
p2: Point = Point()
print(p1.distance(p2)) # 5.0
# Ces lignes seraient détectées par mypy comme des erreurs de type
# p3: Point = Point("trois", "quatre") # Erreur: argument doit être float
# p1.deplacer("cinq", 6) # Erreur: arguments doivent être float
Bonnes pratiques avec les Type Hints¶
1. Utilisez des docstrings complètes¶
Les type hints ne remplacent pas les docstrings :
def calculer_moyenne(notes: list[float]) -> float:
"""Calcule la moyenne d'une liste de notes.
Args:
notes: Liste de notes numériques (doivent être entre 0 et 20)
Returns:
La moyenne des notes
Raises:
ValueError: Si une note est en dehors de l'intervalle [0, 20]
"""
if any(n < 0 or n > 20 for n in notes):
raise ValueError("Toutes les notes doivent être entre 0 et 20")
return sum(notes) / len(notes)
2. Utilisez des types spécifiques plutôt que Any¶
Évitez d’utiliser typing.Any sauf lorsque vraiment nécessaire :
# Mauvais : trop vague
def traiter_donnees(donnees: Any) -> None:
pass
# Bon : plus précis
def traiter_donnees(donnees: list[dict[str, Union[int, str]]]) -> None:
pass
3. Utilisez des alias de type pour les structures complexes¶
Pour améliorer la lisibilité :
from typing import TypeAlias
Note: TypeAlias = float # Une note est un nombre à virgule flottante
EtudiantID: TypeAlias = str # Un identifiant d'étudiant est une chaîne
class Etudiant:
def __init__(self, id: EtudiantID, notes: list[Note]):
self.id = id
self.notes = notes
4. Utilisez des protocoles pour le duck typing¶
Pour les objets qui doivent avoir certains attributs/méthodes :
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
def afficher(obj: Drawable) -> None:
obj.draw()
# Peut être utilisé avec n'importe quel objet ayant une méthode draw()
class Cercle:
def draw(self) -> None:
print("Dessin d'un cercle")
afficher(Cercle()) # Fonctionne
5. Utilisez des annotations pour les attributs de classe¶
Pour documenter les attributs de classe :
class Configuration:
debug: bool = False
timeout: float = 30.0
max_connexions: int = 100
def __init__(self, n_connections: int):
if 0 < n_connections <= Configuration.max_connexions:
self.n_connexions = n_connections
else:
self.n_connexions = Configuration.max_connexions
Outils pour les Type Hints¶
1. mypy - Vérificateur de type statique¶
Installez avec :
Utilisation :
Exemple de sortie :
mon_fichier.py:5: error: Argument 1 to "Personne" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 2 source files)
2. pyright - Alternative à mypy¶
Installez avec :
Utilisation :
3. IDE Support¶
La plupart des IDE modernes supportent les type hints :
- VS Code : Avec l’extension Python
- PyCharm : Support intégré
- Sublime Text : Avec des plugins comme LSP-python
4. pytype - Vérificateur de Google¶
Installez avec :
Utilisation :
Exercice pratique avec Type Hints¶
Complétez le code des classes suivantes, et testez-les. Utilisez mypy pour vérifier que votre implémentation respecte
les types déclarés.
-
Classe CompteBancaire :
-
Classe Etudiant :
-
Fonction de tri personnalisée :
Note :from typing import Callable, TypeVar T = TypeVar("T") def tri_personnalise( liste: list[T], cle: Callable[[T], float] ) -> list[T]: """Trie une liste en utilisant une clé de comparaison.""" passT = TypeVar("T")est une déclaration de type générique. Plus de détails ici.
Annotations de Type vs Attributs : Ne confondez pas Déclaration et Création¶
Une confusion fréquente en Python consiste à croire qu’une annotation de type (type hint) crée un attribut. Il est crucial de distinguer l’ annotation (qui décrit la forme) de l’ assignation (qui crée la donnée).
1. L’annotation seule : Une déclaration (Type Hinting)¶
Lorsque vous écrivez une variable suivie de son type, sans le signe =, vous ne créez pas d’attribut en mémoire. Vous
faites une déclaration.
- Ce que cela fait : Cela informe les outils de développement (IDE comme VS Code, Pycharm) et les analyseurs
statiques (comme
mypy) que si l’attributnomest utilisé, il devrait être une chaîne de caractères (str). - Ce que cela NE fait PAS : Cela ne crée pas la variable. Si vous essayez d’accéder à
Personne.nomouma_personne.nomimmédiatement, Python lèvera uneAttributeError. - En mémoire : L’information est stockée dans le dictionnaire spécial
__annotations__de la classe, mais l’objet n’existe pas encore.
2. L’annotation avec assignation : L’attribut de classe¶
Si vous ajoutez une valeur après l’annotation, vous créez un véritable attribut stocké au niveau de la classe.
- Ce que cela fait : Vous créez un attribut de classe. Toutes les instances de
Personnepartageront, par défaut, cette même valeur. - Usage : On l’utilise pour définir des constantes ou des valeurs par défaut communes à tous les objets de la classe.
3. L’assignation via self : L’attribut d’instance¶
C’est la manière standard de définir les propriétés d’un objet dans un constructeur (__init__).
class Personne:
def __init__(self, nom_initial: str):
self.nom = nom_initial # Assignation sur l'instance
- Ce que cela fait : L’attribut est créé dans l’espace mémoire de l’ objet spécifique (l’instance). Chaque personne a son propre nom, indépendant des autres.
- Usage : C’est la règle d’or pour stocker l’ état d’un objet (ses données propres).
Synthèse comparative¶
Le tableau suivant récapitule les trois cas de figure rencontrés en programmation orientée objet :
| Syntaxe | Terme technique | Ce qui est créé en mémoire | Portée (Scope) | Usage principal |
|---|---|---|---|---|
nom: str |
Annotation seule | Rien (juste une note) | Nulle (statique) | Déclarer une exigence de type pour l’analyse. |
nom = "Valeur" |
Attribut de classe | Une variable partagée | Classe | Constantes ou valeurs par défaut communes. |
self.nom = "Valeur" |
Attribut d’instance | Une variable propre à l’objet | Instance | État unique de l’objet (nom, âge, etc.). |
Astuce de débogage
Si votre programme crash avec une
AttributeError: 'Personne' object has no attribute 'nom', c’est généralement parce que vous avez seulement annoté
le type (nom: str) sans jamais l’ assigner réellement dans un __init__ ou ailleurs.
Conclusion¶
Les type hints sont un outil puissant qui améliore :
- La lisibilité : La signature des fonctions est plus claire
- La maintenabilité : Moins de vérifications manuelles de types
- La sécurité : Détection d’erreurs avant l’exécution
- L’expérience développeur : Meilleure intégration avec les IDE
En combinant type hints et propriétés, vous obtenez un code :
- Bien documenté
- Facile à maintenir
- Sûr grâce aux validations
- Agréable à utiliser grâce à une API propre
Les type hints ne remplacent pas complètement les vérifications à l’exécution (qui restent nécessaires pour la sécurité), mais ils forment un excellent complément qui améliore significativement la qualité du code.