Fix ImportError: cannot import name from partially initialized module (circular import) in Python

intermediate🐍 Python2026-03-22| Python 3.x, all operating systems (Linux, macOS, Windows), any project using multiple modules

Error Message

ImportError: cannot import name 'X' from partially initialized module 'Y' (most likely due to a circular import)
#python#import#circular-import#module

The error hits you at startup

Your app was working fine. You added a new import, and now it won't even start:

ImportError: cannot import name 'UserService' from partially initialized module 'app.services' (most likely due to a circular import)

The parenthetical "most likely due to a circular import" is Python being polite. It is a circular import. Module A imports from module B, and module B imports back from module A. Python starts loading A, hits the import of B, starts loading B, hits the import of A again, and gets a half-baked module. Whatever name you tried to import doesn't exist yet.

Find the cycle first

Before touching anything, read the full traceback β€” not just the last line:

Traceback (most recent call last):
  File "main.py", line 1, in <module>
    from app.routes import user_routes
  File "/app/routes.py", line 3, in <module>
    from app.services import UserService
  File "/app/services.py", line 2, in <module>
    from app.models import User
  File "/app/models.py", line 4, in <module>
    from app.services import UserService
ImportError: cannot import name 'UserService' from partially initialized module 'app.services'

Read it bottom-up. models.py imports from services.py, but services.py was already mid-import when Python got here. Cycle found: services β†’ models β†’ services.

On larger projects where the chain isn't obvious, drop a temporary debug line at the top of suspected modules:

# Temporary debug β€” remove once you've found the cycle
import sys
print(f"Loading {__name__}, already loaded: {[m for m in sys.modules if 'app' in m]}")

Run your entry point and watch the print order. The cycle will show itself within a few lines.

Three ways to break the cycle

Option 1: Move the import inside the function

Fastest fix. Instead of importing at module level, import inside the function that actually needs it:

# services.py β€” BEFORE (causes circular import)
from app.models import User

class UserService:
    def get_user(self, user_id):
        return User.query.get(user_id)

# services.py β€” AFTER (deferred import)
class UserService:
    def get_user(self, user_id):
        from app.models import User  # Import here, not at module level
        return User.query.get(user_id)

Python only executes the import when get_user() is called, by which point both modules are fully loaded. It's not the cleanest pattern, but it's explicit and it works right now.

Option 2: Extract shared code to a third module

The proper architectural fix. When A and B import from each other, they almost always share a concept that belongs in its own module. Pull it out:

# Before:
# models.py imports from services.py
# services.py imports from models.py

# After: create app/base.py or app/types.py
# base.py
from dataclasses import dataclass

@dataclass
class UserData:
    id: int
    email: str

# models.py
from app.base import UserData  # No more services import needed

# services.py
from app.base import UserData  # No more models import needed

The circular import was a signal. Those two modules were too tightly coupled, and this extraction fixes the underlying design, not just the symptom.

Option 3: Use TYPE_CHECKING for type hints

Many circular imports in modern Python exist purely for type annotations β€” the import is never needed at runtime. Guard it with TYPE_CHECKING:

from __future__ import annotations  # Makes all annotations strings at runtime
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from app.services import UserService  # Only imported during type checking

class User:
    def get_service(self) -> 'UserService':  # Works fine
        ...

TYPE_CHECKING is always False at runtime β€” mypy and pyright treat it as True during static analysis. No circular import at runtime, full type safety at check time.

The TYPE_CHECKING pattern in full

Worth showing a complete before/after, because this pattern eliminates a surprisingly large number of real-world cycles:

# Without fix β€” circular import
# order.py
from app.customer import Customer

class Order:
    def __init__(self, customer: Customer):
        self.customer = customer

# customer.py
from app.order import Order

class Customer:
    def get_orders(self) -> list[Order]:
        ...
# With fix
# order.py
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from app.customer import Customer

class Order:
    def __init__(self, customer: Customer):  # Annotation is now a string
        self.customer = customer

# customer.py
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from app.order import Order

class Customer:
    def get_orders(self) -> list[Order]:  # Works
        ...

Verify the fix

Don't just restart and hope. Test each module in isolation from your project root:

python -c "from app.services import UserService; print('OK')"
python -c "from app.models import User; print('OK')"
python -c "from app.routes import user_routes; print('OK')"

Then run your full test suite:

python -m pytest tests/ -x  # -x stops at first failure

To catch import cycles automatically, pylint has built-in cycle detection:

pip install pylint
pylint app/ --disable=all --enable=R0401  # R0401 = cyclic-import

Stop it from coming back

Circular imports creep back in on bigger teams. A few habits that hold the line:

  • Layer your imports: pick a dependency direction (e.g., routes β†’ services β†’ models β†’ base) and never import upward
  • Add cycle detection to CI: run pylint --enable=R0401 in your pipeline so cycles get caught before merge
  • Keep __init__.py lean: re-exporting everything from a package's __init__.py is the most common source of hidden cycles
  • Default to TYPE_CHECKING for annotations β€” add it to your code review checklist

Still broken? Check your .pyc cache

If the cycle is gone but the error persists, stale bytecode may be replaying an old import graph. Clear it:

find . -name '*.pyc' -delete
find . -name '__pycache__' -type d -exec rm -rf {} +
python main.py

Nine times out of ten, one of the three approaches above gets you past this without touching your core logic.

Related Error Notes