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=R0401in your pipeline so cycles get caught before merge - Keep
__init__.pylean: re-exporting everything from a package's__init__.pyis the most common source of hidden cycles - Default to
TYPE_CHECKINGfor 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.

