Vous lancez votre script Python, et une erreur stoppe tout : ImportError: cannot import name 'func_a' from 'module_a' (most likely due to a circular import). Le problème vient d’une boucle de dépendance entre deux modules qui tentent de s’importer mutuellement. Ce type de conflit surgit souvent quand un projet grossit sans qu’on repense l’architecture des fichiers. Voyons comment diagnostiquer et résoudre ces imports circulaires avec des techniques concrètes.
Le mécanisme interne de Python qui provoque l’import circulaire
Avant de corriger, il faut comprendre ce qui se passe sous le capot. Quand Python exécute import module_b depuis module_a, il enregistre module_a dans sys.modules comme « en cours de chargement ». Le fichier module_b démarre alors son propre chargement.
A lire aussi : 30 1.1 introuvable sur votre réseau domestique : diagnostic complet des pannes
Si module_b contient from module_a import func_a, Python consulte sys.modules et trouve module_a, mais dans un état incomplet. La fonction func_a n’a pas encore été définie, car l’exécution de module_a s’est interrompue pour charger module_b. Résultat : Python travaille avec un module partiellement initialisé, et l’import échoue.
Depuis Python 3.11, le message d’erreur est plus explicite. L’exception ImportError inclut la mention « most likely due to a circular import », ce qui facilite le diagnostic dans des projets comportant des dizaines de fichiers.
A lire également : Webmail rectorat Montpellier en 2026 : nouveautés, interface et bonnes pratiques

Import différé dans une fonction Python : la solution rapide
La technique la plus directe consiste à déplacer l’import à l’intérieur de la fonction qui en a besoin, au lieu de le placer en haut du fichier. On parle d’import différé (ou lazy import).
Prenons un exemple. Deux fichiers, user.py et task.py, dépendent l’un de l’autre :
user.py définit une classe User qui appelle une fonction de task.py. Et task.py a besoin de User pour associer une tâche à un utilisateur. Au lieu d’écrire from task import create_task en haut de user.py, placez cet import dans la méthode qui l’utilise :
def assign_task(self, data): from task import create_task ...
Au moment où Python exécute cette méthode, les deux modules sont déjà complètement chargés. L’import ne se déclenche qu’à l’appel de la fonction, pas au chargement du fichier. Le cycle est rompu.
Cette approche a un coût : la lisibilité. Les imports dispersés dans le code sont plus difficiles à repérer lors d’une revue. Réservez cette technique aux cas où une refactorisation plus profonde n’est pas envisageable immédiatement.
Restructurer le code avec un module tiers pour casser la boucle
L’import différé résout le symptôme. Pour traiter la cause, il faut repenser l’architecture. La stratégie la plus fiable consiste à extraire les éléments partagés dans un troisième module.
Imaginons que user.py et task.py partagent un type de donnée ou une fonction utilitaire. Créez un fichier shared.py (ou types.py, base.py, selon votre convention) qui contient ces éléments communs. Les deux modules importent depuis shared.py, mais ne s’importent plus entre eux.
shared.pycontient les classes de base, les types de données ou les constantes utilisées par plusieurs modulesuser.pyimporte depuisshared.pyet ne connaît plustask.pydirectementtask.pyfait de même, en important uniquement depuisshared.py- Si
user.pydoit malgré tout appeler du code detask.py, passez la référence en paramètre de fonction (injection de dépendance)
Cette approche rend le graphe de dépendances acyclique. Chaque module pointe vers shared.py sans créer de boucle.
Import circulaire dans une application Flask : le piège du singleton global
Les projets Flask illustrent parfaitement le problème. Un schéma classique consiste à créer l’instance app = Flask(__name__) dans __init__.py, puis à importer cette instance dans models.py et dans les blueprints. Ces fichiers sont eux-mêmes importés par __init__.py pour enregistrer les routes.
Le cercle est immédiat : __init__.py importe models.py, qui importe app depuis __init__.py.
La solution recommandée dans les versions récentes de Flask repose sur le pattern application factory. Au lieu d’un objet app global, vous créez une fonction create_app() qui instancie et configure l’application. Les blueprints et les extensions n’importent plus l’instance directement.
Pour accéder à l’application dans un blueprint ou un modèle, utilisez current_app fourni par Flask. Ce proxy pointe vers l’application active sans nécessiter d’import direct. L’inversion de contrôle remplace le singleton global et élimine la dépendance circulaire à la racine.

Détecter les imports circulaires avant qu’ils ne cassent le code
Attendre l’ImportError en production n’est pas une stratégie. Plusieurs outils permettent de repérer les cycles de dépendances en amont.
pylintsignale les imports circulaires avec le messagecyclic-importlors de l’analyse statique du projetpydepsgénère un graphe visuel des dépendances entre modules, ce qui rend les boucles immédiatement repérables- Un test simple : lancez
python -c "import votre_module"pour chaque module du projet dans un script de CI, les erreurs d’import circulaire apparaissent sans exécuter le code métier
Intégrer cette vérification dans votre pipeline d’intégration continue empêche les régressions. Un import circulaire introduit dans une pull request est détecté avant la fusion.
Références faibles et gestion mémoire liée aux imports croisés
Les imports circulaires ne causent pas uniquement des erreurs au chargement. Quand plusieurs modules partagent des singletons ou des caches en s’importent mutuellement, ils créent des graphes d’objets fortement connectés. Le ramasse-miettes de Python (garbage collector) peine alors à libérer la mémoire, car les références circulaires entre objets retardent la collecte jusqu’au prochain cycle de GC.
Une pratique avancée consiste à remplacer certaines références croisées par des références faibles avec le module weakref. Quand un objet de domaine doit pointer vers un autre sans empêcher sa libération, weakref.ref() évite de maintenir une référence forte. Le garbage collector peut alors nettoyer les objets dès qu’ils ne sont plus utilisés activement.
Cette technique s’applique surtout aux couches basses du code (objets de domaine, caches internes) et non aux imports de modules eux-mêmes. Elle complète la restructuration architecturale en limitant les effets secondaires des dépendances croisées sur la mémoire.
La plupart des imports circulaires en Python révèlent un problème d’architecture, pas un manque de technique. Extraire les dépendances communes, différer un import quand la refactorisation n’est pas possible, adopter un pattern factory dans Flask : ces trois réflexes couvrent la grande majorité des situations. Le reste est affaire d’outillage et de discipline dans le pipeline de développement.

