Technique Python typing : stop aux imports lourds et circulaires

Emmanuel Chomarat Photo de Emmanuel Chomarat

Emmanuel Chomarat

Python typing : stop aux imports lourds et circulaires

Si vous travaillez avec les annotations de type en Python, vous avez probablement rencontré des problèmes de références circulaires ou des imports lourds qui ralentissent le démarrage de votre application. Dans cet article, nous allons explorer deux outils puissants qui résolvent ces problèmes : from __future__ import annotations et typing.TYPE_CHECKING.

Le problème des annotations de type

Par défaut, Python évalue les annotations de type au moment de l'import du module. Cela peut créer plusieurs problèmes :

  1. Références circulaires impossibles
  2. Imports lourds qui ralentissent le démarrage
  3. Dépendances obligatoires même pour le typage uniquement

Voyons comment résoudre ces problèmes.

Solution 1 : from __future__ import annotations

Cette fonctionnalité change la manière dont Python traite les annotations de type. Au lieu de les évaluer immédiatement, Python les stocke sous forme de chaînes de caractères.

Exemple : Références avant (forward references)

from __future__ import annotations

class Node:
    def __init__(self, value: int, next: Node = None):
        self.value = value
        self.next = next

Sans cet import, vous auriez besoin d'écrire next: 'Node' avec des guillemets.

Avantages

  • Syntaxe plus propre pour les auto-références
  • Évite certains imports circulaires
  • Imports plus rapides (les annotations ne sont pas évaluées)
  • Compatible avec les types non importés

Est-ce utile en Python 3.12 ?

Oui ! Même si Python 3.12 supporte nativement list[str] et dict[str, int], cet import reste utile pour :

  • Les références avant
  • Les imports circulaires
  • L'optimisation des performances d'import
  • Garder les annotations comme métadonnées plutôt que code runtime

Python 3.13+ : Optionnel

  • Le comportement par défaut est lazy (et va même plus loin que juste des chaines de caractères)
  • Peut être utile pour la compatibilité ascendante
  • Certains projets le gardent pour cohérence

Solution 2 : Combiner avec TYPE_CHECKING

La véritable puissance vient de la combinaison avec typing.TYPE_CHECKING. Cette constante est toujours False au runtime, mais les linters / vérificateurs de type (mypy, pyright, pyrefly, ty) la traitent comme True.

Pattern recommandé

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import pandas as pd
    from heavy_package import HeavyClass
    from circular_module import OtherClass

def process_data(df: pd.DataFrame) -> None:
    # pandas n'est PAS importé au runtime !
    pass

def handle_object(obj: HeavyClass) -> None:
    # HeavyClass n'est PAS importée au runtime !
    pass

Cas d'usage pratiques

1. Éviter les imports circulaires

# module_a.py
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from module_b import ClassB

class ClassA:
    def work_with(self, other: ClassB) -> None:
        pass

2. Dépendances optionnelles

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import tensorflow as tf  # L'utilisateur n'a peut-être pas tensorflow

def train_model(model: tf.keras.Model | None = None) -> None:
    if model is not None:
        import tensorflow as tf  # Import seulement si utilisé
        # ... utiliser tensorflow

3. Optimiser les performances

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import torch
    import transformers
    # Bibliothèques lourdes importées uniquement pour le typage

def load_model(config: transformers.PretrainedConfig) -> None:
    pass

Comment Python résout les types

Lorsque vous utilisez des annotations sous forme de chaînes, Python les résout en utilisant le namespace du module où l'annotation a été définie.

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from package_a.models import User as UserA
    from package_b.models import User as UserB
    import package_b.models  # Nécessaire pour process_b

def process_a(user: UserA) -> None:
    # Résolu via l'espace de noms local
    pass

def process_b(user: package_b.models.User) -> None:
    # Nom complètement qualifié - toujours sans ambiguïté
    pass

Pour obtenir les types réels au runtime :

import typing

hints = typing.get_type_hints(process_a)
# Évalue "UserA" dans l'espace de noms du module

Attention : Quand vous avez besoin du type au runtime

Si vous devez utiliser le type au runtime (pour isinstance(), créer des instances, etc.), vous devez toujours l'importer normalement :

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from package_a import User

def process(user: User) -> None:
    from package_a import User  # Import réel nécessaire !
    if isinstance(user, User):
        # ... faire quelque chose
        pass

Bonnes pratiques

  1. Utilisez toujours les deux ensemble : from __future__ import annotations + TYPE_CHECKING
  2. Préférez les noms complets : package.module.Classe évite les ambiguïtés
  3. Importez au runtime si nécessaire : Pour isinstance() et la création d'instances
  4. Documentez vos dépendances optionnelles : Indiquez clairement quelles bibliothèques sont nécessaires

Conclusion

La combinaison de from __future__ import annotations et typing.TYPE_CHECKING est un pattern de best practice en Python moderne. Elle vous permet de :

  • Écrire des annotations de type propres et complètes
  • Éviter les imports circulaires
  • Optimiser les performances de démarrage
  • Gérer les dépendances optionnelles élégamment

Ces outils sont particulièrement précieux dans les grandes bases de code où la gestion des dépendances et des performances d'import devient critique.

Adoptez ce pattern dès maintenant, et vos projets Python seront plus maintenables et plus performants !

Tags : Python