Import circulaire Python : comment le corriger (exemples concrets)
Publié le
Mis à jour le
Un import circulaire (circular import) survient lorsque le module A importe le module B pendant que B (directement ou via une chaîne) importe aussi A. Python ne tourne en général pas en boucle infinie. Vous obtenez plutôt un import partiel : un module est encore en cours de chargement quand l'autre tente d'en lire un nom, d'où ImportError / AttributeError, ou des objets à moitié initialisés (partially initialized module).
Correction rapide (commencer ici)
| Situation | Première action |
|---|---|
| Deux modules s'importent mutuellement au niveau supérieur | Déplacer le code partagé dans un troisième module importable par les deux |
| Vous n'avez besoin d'un nom qu'à l'intérieur d'une fonction | Importer dans cette fonction (import paresseux / lazy import) |
| Vous n'avez besoin que de types pour les annotations | Utiliser from typing import TYPE_CHECKING et coter les annotations si besoin |
| Gros package avec cycles profonds | Redesigner les dépendances pour que les couches basses n'importent jamais les couches hautes |
Motif minimal qui débloque souvent immédiatement :
# Instead of a top-level cycle:
# models.py imports services.py
# services.py imports models.py
# services.py — import only where used
def process_user(user_id: int):
from models import User # lazy import breaks the load-time cycle
return User.get(user_id)Si cela ressemble à un pansement, continuez la lecture. La solution durable est presque toujours le sens des dépendances, pas une astuce d'import.
- Runcell Science : l'alternative open source à Claude Science pour la recherche
- Empêcher un Mac de se mettre en veille : capot fermé, Codex et Claude Code
- OpenClaw vs ZeroClaw vs Pi Agent vs Nanobot : quelle stack d’agents IA choisir en 2026 ?
- Comment Claude Code analyse un notebook Jupyter en Data Science : capacités réelles et limites
- Claude Code routines : triggers, cron jobs et automatisation d’agents IA
- Claude Code Desktop : activer le mode Bypass permissions
- Comment Créer Deux Agents Python avec le Protocole A2A de Google - Tutoriel Étape par Étape
- Top 10 bibliothèques de visualisation de données en Python en pleine croissance en 2025
À quoi ressemble un import circulaire
Exemple minimal qui échoue
Créez deux modules frères :
# a.py
import b
def hello_from_a():
return "A"
print("a loaded", b.hello_from_b())# b.py
import a
def hello_from_b():
return "B"
print("b loaded", a.hello_from_a())Exécution :
python a.pyÉchec typique (le libellé varie selon la version de Python) :
ImportError: cannot import name 'hello_from_a' from partially initialized module 'a'
(most likely due to a circular import)Ce message est la forme « requête de recherche » du problème. Python a commencé à charger a, qui a commencé à charger b, qui a tenté de terminer l'import depuis a avant que a n'ait fini de s'exécuter.
Ce qui se passe réellement
Lorsqu'il importe un module, Python :
- Crée un objet module vide et le place dans
sys.modules. - Exécute le corps du module de haut en bas.
- Ce n'est qu'après la fin de ce corps que tous les noms de niveau supérieur sont garantis d'exister.
Pendant un cycle, l'étape 2 du module A est encore en cours quand B demande quelque chose défini plus bas dans A. Le nom manque → erreur d'import. C'est un échec d'initialisation partielle, pas une boucle sans fin.
Démarche de diagnostic
Suivez cet ordre. Arrêtez-vous dès que le cycle a disparu.
-
Reproduire avec un point d'entrée propre
Lancez le même fichier que mentionnent les erreurs Google/recherche (python -m package.moduleou le chemin du script). Les imports circulaires sont sensibles au point d'entrée. -
Lire la traceback complète de bas en haut
Les dernières frames montrent en généralmodule_xqui importemodule_yalors quemodule_yest déjà sur la pile. -
Esquisser un graphe de dépendances à sens unique
Listez chaque module et ce qu'il importe. Toute arête qui pointe « vers le haut » (modèles de base vers app/UI/API) est suspecte. -
Classer le besoin
- Valeur / fonction à l'exécution → restructurer ou import paresseux
- Type uniquement →
TYPE_CHECKING - Constantes / modèles partagés → extraire vers un module feuille
-
Appliquer la plus petite correction correcte
Préférez extraire un module partagé > import paresseux > refonte du package. Évitez le cargo-cult « les imports absolus vont tout résoudre ». -
Relancer depuis le même point d'entrée
Vérifiez à la fois le stylepython a.pyet les points d'entrée package-msi vous utilisez les deux.
Correction 1 : extraire un troisième module (meilleur défaut)
Quand models et services ont tous deux besoin de User, ne les faites pas s'importer mutuellement. Placez User dans un module feuille qui ne dépend d'aucune couche supérieure.
app/
models/user.py # leaf: no import from services
services/billing.py # imports models.user
api/routes.py # imports services# models/user.py
class User:
def __init__(self, user_id: int, email: str):
self.user_id = user_id
self.email = email
@classmethod
def get(cls, user_id: int) -> "User":
return cls(user_id, f"user{user_id}@example.com")# services/billing.py
from models.user import User
def invoice(user_id: int) -> str:
user = User.get(user_id)
return f"Invoice for {user.email}"# api/routes.py
from services.billing import invoice
def handle(user_id: int) -> str:
return invoice(user_id)Le sens des dépendances est à sens unique : api → services → models. Les cycles disparaissent car rien en bas n'importe rien en haut.
Correction 2 : imports paresseux (locaux)
Importez dans la fonction ou la méthode qui a besoin du symbole lorsqu'un cycle temporaire est difficile à démêler.
# reporters.py
def build_report(order_id: str) -> dict:
from orders import Order # imported at call time, not module load time
order = Order.load(order_id)
return {"order_id": order.id, "total": order.total}# orders.py
class Order:
def __init__(self, id: str, total: float):
self.id = id
self.total = total
@classmethod
def load(cls, order_id: str) -> "Order":
return cls(order_id, 19.99)
def pretty(self) -> str:
from reporters import build_report # only if you truly need this edge
return str(build_report(self.id))Quand les imports paresseux conviennent
- Casser rapidement un cycle tenace dans du code legacy
- Dépendances lourdes optionnelles (importer uniquement sur le chemin de code qui en a besoin)
Quand les éviter comme design à long terme
- Chemins chauds où le coût d'import compte (souvent mineur, mais mesurable)
- Architecture qui continue de créer de nouveaux cycles ; extrayez plutôt des modules
Correction 3 : TYPE_CHECKING pour les imports d'annotations uniquement
Si la seule raison de l'import est le typage, tenez-le hors du runtime :
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from models.user import User # not imported at runtime
def notify(user: User, message: str) -> None:
print(user.email, message)Avec from __future__ import annotations (ou des annotations "User" cotées), Python n'a pas besoin de User à l'exécution pour que l'annotation fonctionne. C'est le bon outil pour les cycles liés aux type hints, pas pour appeler des méthodes sur User.
Correction 4 : injecter les dépendances au lieu de les importer
Pour des services qui « ont besoin l'un de l'autre », passez les collaborateurs en argument plutôt que d'importer globalement.
# notifications.py
class Notifier:
def send(self, email: str, body: str) -> None:
print(f"to={email} body={body}")
# checkout.py
class Checkout:
def __init__(self, notifier: "Notifier"):
self.notifier = notifier
def complete(self, email: str) -> None:
# business logic...
self.notifier.send(email, "Order complete")# main.py
from notifications import Notifier
from checkout import Checkout
checkout = Checkout(Notifier())
checkout.complete("a@example.com")Ni notifications ni checkout n'importe l'autre. La composition à la frontière (main.py) gère le câblage. Même idée que l'injection par constructeur dans les applications et frameworks plus larges.
Pièges courants (conseils qui ne cassent pas les cycles)
Ces conseils apparaissent souvent dans d'anciens guides. Ils résolvent d'autres problèmes, pas les imports circulaires.
| Conseil que vous pouvez croiser | Réalité |
|---|---|
| « Utilisez des imports absolus » | Les imports absolus clarifient les chemins. Ils ne suppriment pas une dépendance mutuelle. |
« Définissez __all__ » | Contrôle uniquement la surface d'export de from module import *. Aucun effet sur les cycles. |
« Utilisez toujours importlib.import_module » | L'import dynamique peut retarder le chargement, mais si vous importez encore au mauvais moment, le cycle reste. Préférez un import local explicite ou une restructuration. |
| « Python entre en boucle infinie » | Le résultat habituel est ImportError / initialisation partielle, pas une boucle qui tourne. |
| « Renommez le fichier et croisez les doigts » | Les collisions de noms peuvent provoquer des erreurs d'import confuses, mais renommer seul ne corrige pas une vraie dépendance A↔B. |
Motifs d'organisation de package qui évitent les cycles
Les packages sains ressemblent à un DAG (graphe dirigé acyclique) :
package/
__init__.py # keep thin; avoid importing everything eagerly
domain/ # pure models, no IO
services/ # use domain
adapters/ # DB, HTTP, filesystem
api/ # entrypoints; imports services onlyRègles pratiques :
- Les modules feuilles n'importent rien de l'application.
__init__.pydoit rester mince. Des chaînesfrom .a import */from .b import *avides sont une usine à cycles courante.- Préférez les imports explicites depuis des modules feuilles stables aux réexports « de commodité » qui tirent la moitié du package.
- Exécutez en package lors du développement de bibliothèques :
python -m package.apiévite les bricolages de chemin qui masquent de vrais problèmes de dépendances. Voir aussi comment exécuter des scripts Python.
Si vous déplacez beaucoup de fichiers pendant un refactor, pathlib rend les scripts de déplacement/renommage plus clairs que du collage de chaînes de chemins.
Boîte à outils de débogage
Tracer où le cycle entre
# debug_import.py
import sys
import trace
tracer = trace.Trace(count=False, trace=True)
tracer.runfunc(lambda: __import__("your_package.entry"))Au quotidien, la traceback suffit. Pour de grandes bases de code, des outils aident :
python -X importtime -c "import your_package"— montre l'ordre et le coût des imports- import-linter (tiers) — encoder « services ne doit pas importer api » comme règles CI
- pyright / mypy — attraper tôt les erreurs
TYPE_CHECKINGavec les annotations
Capturer les symptômes d'init partielle dans les tests
def test_package_imports_cleanly():
import importlib
import your_package.api as api
importlib.reload(api) # optional stress
assert hasattr(api, "handle")Si les tests unitaires n'importent que de minuscules modules isolés, ils peuvent manquer des cycles qui n'apparaissent que via le point d'entrée de l'app. Ajoutez un test de fumée qui importe le vrai module d'entrée.
Tableau de décision : quelle correction utiliser ?
| Signal | Préférer |
|---|---|
| Modèle / constante partagé(e) des deux côtés | Extraire un troisième module |
| Un seul chemin d'appel a besoin de l'autre module | Import paresseux dans cette fonction |
| L'import n'existe que pour les vérificateurs de types | TYPE_CHECKING |
| Deux services s'orchestrent mutuellement | Injection de dépendances / callbacks / événements |
| Les cycles reviennent après des rustines | Règles de couches de package + lint en CI |
FAQ
Conclusion
Les imports circulaires sont un problème de graphe de dépendances. Python les expose comme des erreurs d'initialisation partielle, surtout le message familier « most likely due to a circular import ». Corrigez-les en rendant les dépendances à sens unique : extrayez des modules feuilles partagés, n'utilisez l'import paresseux que là où c'est nécessaire, gardez les imports d'annotations derrière TYPE_CHECKING, et câblez les collaborateurs depuis l'extérieur quand deux services ont besoin l'un de l'autre.
Ignorez le folklore — imports absolus, __all__ et renommages de fichiers ne dissolvent pas un vrai cycle. Une fois le graphe propre, les imports redeviennent ennuyeux, ce qui est exactement ce qu'il faut.
Guides connexes
- Type hints Python — motifs d'annotation qui vont de pair avec
TYPE_CHECKING - Python pathlib — gestion de chemins plus claire pendant la réorganisation des packages
- Comment exécuter des scripts Python — points d'entrée (
python,-m) qui affectent le comportement des imports - Python try/except — gérer et diagnostiquer les échecs d'import à l'exécution
- Python dataclasses — modèles de domaine légers qui appartiennent aux modules feuilles
- Décorateurs Python — motifs qui introduisent parfois des effets de bord au moment de l'import