Skip to content
Themen
Python
Python Circular Import: How to Fix It (With Working Examples)

Python Circular Import beheben: ImportError und teilweise initialisierte Module

Veröffentlicht am

Aktualisiert am

Ein zirkulärer Import (Circular Import) entsteht, wenn Modul A Modul B importiert und B (direkt oder über eine Kette) wieder A importiert. Python hängt dabei in der Regel nicht in einer Endlosschleife. Stattdessen bleibt ein Modul teilweise initialisiert: Während es noch geladen wird, liest das andere Modul einen Namen daraus — und es kommt zu ImportError / AttributeError oder halb fertigen Objekten.

Schnelle Lösung (hier starten)

SituationErster Schritt
Zwei Module importieren sich gegenseitig auf Top-LevelGemeinsamen Code in ein drittes Modul ziehen, das beide importieren
Name wird nur in einer Funktion gebrauchtImport in dieser Funktion (Lazy Import)
Nur Typen für Annotationen nötigfrom typing import TYPE_CHECKING und Annotationen bei Bedarf quoten
Großes Package mit tiefen ZyklenAbhängigkeiten so schichten, dass untere Schichten nie nach oben importieren

Minimales Muster, das oft sofort entblockt:

# 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)

Fühlt sich das wie ein Pflaster an, weiterlesen. Die stabile Lösung ist fast immer die Abhängigkeitsrichtung, nicht ein cleverer Import-Trick.

So sieht ein Circular Import aus

Minimales Fehlerbeispiel

Zwei Geschwistermodule anlegen:

# 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())

Ausführen:

python a.py

Typischer Fehler (Formulierung je nach Python-Version):

ImportError: cannot import name 'hello_from_a' from partially initialized module 'a'
(most likely due to a circular import)

Genau diese Meldung treibt die Suche. Python hat begonnen, a zu laden, dadurch b, und b wollte aus a importieren, bevor a fertig ausgeführt war.

Was wirklich schiefgeht

Beim Import eines Moduls:

  1. legt Python ein leeres Modulobjekt an und trägt es in sys.modules ein,
  2. führt den Modulrumpf von oben nach unten aus,
  3. erst danach sind alle Top-Level-Namen garantiert vorhanden.

Im Zyklus läuft Schritt 2 für Modul A noch, während B etwas verlangt, das weiter unten in A definiert wird. Der Name fehlt → Importfehler. Das ist ein teilweise initialisiertes Modul (partial initialization), keine Endlosschleife.

Troubleshooting-Flow

Diese Reihenfolge nutzen. Stoppen, sobald der Zyklus weg ist.

  1. Mit sauberem Entrypoint reproduzieren
    Dieselbe Datei ausführen, die die Fehlermeldung nennt (python -m package.module oder Skriptpfad). Circular Imports hängen stark vom Entrypoint ab.

  2. Traceback von unten nach oben lesen
    Die letzten Frames zeigen meist module_x importiert module_y, während module_y schon auf dem Stack liegt.

  3. Abhängigkeiten skizzieren (eine Richtung)
    Pro Modul notieren, was es importiert. Kanten „nach oben“ (Core/Models → App/UI/API) sind verdächtig.

  4. Bedarf einordnen

    • Laufzeitwert/Funktion → umstrukturieren oder Lazy Import
    • nur Typ → TYPE_CHECKING
    • gemeinsame Konstanten/Modelle → in ein Leaf-Modul extrahieren
  5. Kleinste korrekte Lösung anwenden
    Bevorzugen: gemeinsames Modul extrahieren > Lazy Import > Package-Layout umbauen. Nicht dem Mythos „absolute Imports lösen das“ folgen.

  6. Vom gleichen Entrypoint erneut laufen lassen
    Bei gemischter Nutzung sowohl python a.py als auch -m-Aufrufe prüfen.

Fix 1: Drittes Modul extrahieren (bester Default)

Brauchen models und services beide User, sollen sie sich nicht gegenseitig importieren. User gehört in ein Leaf-Modul ohne Abhängigkeiten zu höheren Schichten.

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)

Abhängigkeitsrichtung ist einseitig: api → services → models. Zyklen verschwinden, weil nichts Unteres etwas Oberes importiert.

Fix 2: Lazy (lokale) Imports

Import in die Funktion oder Methode legen, die das Symbol braucht — sinnvoll, wenn ein temporärer Zyklus schwer entwirrbar ist.

# 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))

Wann Lazy Imports passen

  • hartnäckigen Zyklus in Legacy-Code schnell brechen
  • optionale schwere Abhängigkeiten nur auf dem benötigten Pfad laden

Wann sie kein Langzeit-Design sind

  • Hot Paths, in denen Importkosten messbar stören (meist gering)
  • Architektur, die laufend neue Zyklen produziert — besser Module extrahieren

Fix 3: TYPE_CHECKING für rein annotierende Imports

Dient der Import nur Type Hints, gehört er nicht in die 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)

Mit from __future__ import annotations (oder quoted "User") braucht Python User zur Laufzeit nicht für die Annotation. Das ist das richtige Werkzeug für Zyklen bei Type Hints — nicht, um Methoden auf User aufzurufen.

Fix 4: Abhängigkeiten injizieren statt importieren

Wenn Services „einander brauchen“, Collaborators übergeben statt global zu importieren.

# 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")

Weder notifications noch checkout importieren einander. Die Verdrahtung sitzt am Rand (main.py). Gleiche Idee wie Constructor Injection in größeren Apps und Frameworks.

Typische Fallen (Tipps, die Zyklen nicht lösen)

Die tauchen in älteren Guides oft auf. Sie lösen andere Probleme, keinen Circular Import.

Tipp, den man siehtRealität
„Absolute Imports nutzen“Absolute Imports verbessern Klarheit. Eine gegenseitige Abhängigkeit entfernen sie nicht.
__all__ setzen“Steuert nur die Export-Oberfläche von from module import *. Kein Effekt auf Zyklen.
„Immer importlib.import_moduleDynamischer Import kann das Laden verzögern; zum falschen Zeitpunkt importiert bleibt der Zyklus. Expliziter lokaler import oder Restrukturierung ist klarer.
„Python gerät in eine Endlosschleife“Üblich ist ImportError / partial init, kein spinender Loop.
„Datei umbenennen und hoffen“Namenskollisionen können verwirrende Importfehler erzeugen, beheben aber keine echte A↔B-Abhängigkeit.

Package-Layouts, die Zyklen verhindern

Gesunde Packages wirken wie ein DAG (gerichteter azyklischer Graph):

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 only

Praktische Regeln:

  1. Leaf-Module importieren nichts aus der App.
  2. __init__.py dünn halten. Eager from .a import * / from .b import *-Ketten sind eine häufige Zyklus-Fabrik.
  3. Explizite Imports aus stabilen Leaf-Modulen statt „Bequemlichkeits“-Reexports, die das halbe Package ziehen.
  4. Als Package ausführen, wenn man Libraries entwickelt: python -m package.api vermeidet Path-Hacks, die echte Abhängigkeitsprobleme verdecken. Siehe auch Python-Skripte ausführen.

Beim Umsortieren von Dateien hält pathlib Move/Rename-Skripte klarer als zusammengeklebte String-Pfade.

Debugging-Toolkit

Nachvollziehen, wo der Zyklus greift

# debug_import.py
import sys
import trace
 
tracer = trace.Trace(count=False, trace=True)
tracer.runfunc(lambda: __import__("your_package.entry"))

Im Alltag reicht der Traceback. Bei großen Codebases helfen Tools:

  • python -X importtime -c "import your_package" — Importreihenfolge und Kosten
  • import-linter (Third Party) — Regeln wie „services darf api nicht importieren“ in CI
  • pyright / mypyTYPE_CHECKING-Fehler bei Annotationen früh fangen

Partial-Init-Symptome in Tests abfangen

def test_package_imports_cleanly():
    import importlib
    import your_package.api as api
 
    importlib.reload(api)  # optional stress
    assert hasattr(api, "handle")

Unit-Tests, die nur winzige Module isoliert importieren, verpassen Zyklen, die erst über den App-Entrypoint auftreten. Einen Smoke-Test ergänzen, der das echte Entry-Modul lädt.

Entscheidungstabelle: welcher Fix?

SignalBevorzugen
Gemeinsames Modell/Konstante auf beiden SeitenDrittes Modul extrahieren
Ein Aufrufpfad braucht das andere ModulLazy Import in dieser Funktion
Import nur für Type CheckerTYPE_CHECKING
Zwei Services orchestrieren sichDependency Injection / Callbacks / Events
Zyklen kehren nach Patches zurückSchichtregeln im Package + Linting in CI

FAQ

Fazit

Circular Imports sind ein Problem des Abhängigkeitsgraphen. Python meldet sie als Fehler der teilweisen Initialisierung — besonders mit der bekannten Meldung „most likely due to a circular import“. Beheben lässt sich das durch einseitige Abhängigkeiten: gemeinsame Leaves extrahieren, nur wo nötig lazy importieren, Annotations-Imports hinter TYPE_CHECKING halten und Collaborators von außen verdrahten, wenn zwei Services einander brauchen.

Folklore-Fixes überspringen — absolute Imports, __all__ und Umbenennen lösen keinen echten Zyklus. Ist der Graph sauber, werden Imports wieder langweilig. Genau das will man.

Verwandte Anleitungen