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:
import shop.modelsaddsshop.modelsto the cache and starts running it.- The first line of
modelsimportsshop.services, which starts running. - The first line of
servicesasks forOrderfromshop.models. Python finds that module in the cache, but its code has not reachedclass Orderyet. - 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.totalatax_forargument, andmodelsneeds 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
Hide 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.