Skip to content
← Back to explainers

Python explainer

Fix a Circular Import

About 4 min to read. Allow extra time to work through examples.

Read the partially initialized module error, see why two modules that import each other fail, and fix the cycle by making the imports go one way.

This article uses Python. You can make it your practice mode to browse related content across the site.

Two modules in your shop package import each other. shop/models.py needs a tax helper:

# shop/models.py
from shop.services import tax_for


class Order:
    def __init__(self, amount: float) -> None:
        self.amount = amount

    def total(self) -> float:
        return self.amount + tax_for(self.amount)

shop/services.py needs Order:

# shop/services.py
from shop.models import Order


def tax_for(amount: float) -> float:
    return amount * 0.2


def create_order(amount: float) -> Order:
    return Order(amount)

import shop.models fails:

ImportError: cannot import name 'Order' from partially initialized module 'shop.models' (most likely due to a circular import)

This failure is a circular import. It happens when modules import each other, directly or through a chain. The loop in the imports, where models imports services and services imports models, is an import cycle.

Why the name is missing

Python runs a module's file from top to bottom. It adds the module to the module cache, sys.modules, before it starts. Here is the order of events:

  1. import shop.models adds shop.models to the cache and starts running it.
  2. The first line of models imports shop.services, which starts running.
  3. The first line of services asks for Order from shop.models. Python finds that module in the cache, but its code has not reached class Order yet.
  4. The name is missing, so Python raises ImportError.

"Partially initialized" means the module exists, but only part of its code has run. Importing shop.services first fails the same way, with tax_for as the missing name.

Make the imports go one way

The fix is to remove the cycle. Ask which module should depend on which. Order is data, and services builds on it, so services may import models and models should not import services. tax_for needs neither module, so move it into one of its own:

# shop/tax.py
def tax_for(amount: float) -> float:
    return amount * 0.2

Now models imports tax, and services imports models. Nothing loops back. Most cycles have a fix like this. Find the piece both modules need, and move it into a module that imports neither of them.

Two other fixes help in narrower cases:

  • Pass the dependency in. Give Order.total a tax_for argument, and models needs no import at all. A later article covers this pattern.
  • Import a name for type hints only. If a module uses a name only in annotations, import it under if TYPE_CHECKING:. Type checkers read that block, and Python skips it when the code runs, so the import cannot start a cycle.
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from shop.models import Order


def describe(order: "Order") -> str:
    return f"order of {order.amount}"

The quotes stop Python from evaluating Order when it defines describe.

Check your understanding services calls Order(amount). Can you break the cycle by moving its import under if TYPE_CHECKING:? Show answer

No. TYPE_CHECKING is False when the code runs, so the import never happens. Order is then undefined, so calling create_order raises NameError. Use TYPE_CHECKING only for names that appear in annotations alone. Here, move tax_for out instead.

Common mistakes

Moving an import to the bottom of the file, or into a function. The cycle is still there. Only the timing changes, so the import can still fail if code uses the name before both modules have finished running. An import inside a function is fine for a slow, optional library. As a cycle fix, it only hides the loop.

Trusting the test suite to catch it. Some cycles fail only when you import one particular module first. Tests run in one process with one module cache. If an earlier test loaded the modules in the order that works, later tests never see the failure. Import each module on its own in a fresh process, for example python -c "import shop.models".

Find cycles in a whole project

In a large project, find cycles with a tool instead of by reading. The next two exercises work on an import graph given as data, a dictionary that maps each module to the modules it imports. The first finds one cycle, and the second lists every cycle once.

Going further

Python 3.13 and newer print a different message for two top-level modules in your script's folder, outside any package. The error suggests renaming your file, in case it hides a library of the same name. Python 3.14 evaluates annotations only when something reads them, so the quotes around "Order" are optional there. See the import system reference and typing.TYPE_CHECKING.

step 9 of 41

Modules, Packaging and Tooling

Up next Find One Import Cycle