Python ImportError: cannot import name — A Complete Guide to Fixing Circular Imports
This post is not about
ModuleNotFoundError.ModuleNotFoundError: No module named 'models'means Python cannot find the module file itself (path, install, or typo). What this post covers is circular imports: the module is found, but a name inside it cannot be imported. (If you cannot find the module at all → see ModuleNotFoundError troubleshooting guide.)
"The name is right — why won't it import?"
You clearly have a User class in models.py, the spelling is correct, and yet you get this error.
ImportError: cannot import name 'User' from partially initialized module 'models' (most likely due to a circular import) (/app/models.py)The first time you see this, you open models.py over and over thinking you misspelled the class name. The real clue is already in the parentheses.
(most likely due to a circular import)— this one line is the key.
Python is telling you, quite helpfully, that this is most likely a circular import. And the phrase partially initialized module describes exactly what happened. Read that one line of the error carefully and the problem location becomes obvious.
Anatomy of the error line: why "partially initialized"?
When Python imports a module, it executes the file top to bottom once, registering classes and functions in memory as it goes. The catch: if that execution hits another import in the middle, it pauses there.
Let's walk through what happens when a.py imports b.py and b.py imports a.py again.
① Someone does import a → a.py starts executing
② First line of a.py is import b → a pauses and jumps to b.py
③ b.py runs from a import something
→ a is still at step ② — it has not reached the line that defines something!
→ a is in a "partially initialized" state
④ Hence: cannot import name 'something' from partially initialized module 'a'In other words, the name is not missing — you tried to grab it before it was defined. It's a timing problem. Once this picture is in your head, the rest is easy.
Starting with Python 3.12+, import tracebacks are more helpful, making it much easier to trace which line started the cycle. Look at the innermost frame of the traceback and you'll see which module was calling whom when it got stuck.
Three typical circular-import patterns
① Bidirectional import between modules A ↔ B
This is the most common case. Copy-paste and reproduce it yourself.
# a.py
from b import func_b
def func_a():
return "a"
print(func_b())# b.py
from a import func_a # 💥 a는 아직 func_a 정의 전
def func_b():
return "b"Run python a.py → cannot import name 'func_a' from partially initialized module 'a'.
② Over-eager re-exports in __init__.py
Trying to make a package convenient by pulling everything into __init__.py often creates a cycle. A staple in FastAPI and SQLAlchemy projects.
# models/__init__.py
from .user import User
from .order import Order # order.py가 다시 models의 User를 import하면 순환# models/order.py
from models import User # 💥 __init__.py가 아직 실행 중③ Imports pulled in only for type hints
The import is not needed at runtime at all — you imported it only to write a type hint, and that created a cycle. Common between Pydantic and SQLAlchemy models.
# user.py
from order import Order # 오직 아래 타입힌트용
class User:
def latest_order(self) -> Order: # 사실 런타임엔 Order 객체 필요 없음
...# order.py
from user import User # 💥 user ↔ order 순환
class Order:
owner: User5 fixes (before / after copy-paste code)
Fix 1. Local import inside the function
Move the import off the top of the module and into the function that actually uses it. By the time the function is called, both modules are already initialized, so it's safe.
# before — b.py
from a import func_a
def func_b():
return func_a()# after — b.py
def func_b():
from a import func_a # 호출 시점에 import → 순환 회피
return func_a()Fix 2. Move the import to the bottom of the file
If you import after a.py has finished all the definitions it needs, you avoid the partially-initialized problem.
# after — a.py
def func_a():
return "a"
from b import func_b # 정의가 끝난 뒤 import
print(func_b())Fix 3. TYPE_CHECKING + string annotations (the standard fix for type-hint cycles)
If the cycle exists only because of type hints, as in pattern ③, this is the right answer. TYPE_CHECKING is False at runtime, so the import never actually happens — only type checkers (mypy, Pyright) and the IDE see it.
# before — user.py
from order import Order
class User:
def latest_order(self) -> Order:
...# after — user.py
from typing import TYPE_CHECKING
if TYPE_CHECKING: # 런타임엔 실행 안 됨 → 순환 끊김
from order import Order
class User:
def latest_order(self) -> "Order": # 문자열 어노테이션
...Even simpler: add one line at the top of the file. Thanks to PEP 563, every annotation is treated as a string automatically, so it works without quotes.
# user.py 맨 위 한 줄
from __future__ import annotations # 모든 타입힌트를 지연 평가
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from order import Order
class User:
def latest_order(self) -> Order: # 따옴표 없이 OK
...Fix 4. Extract a shared base module
If A and B share code and that sharing created the cycle, pull the shared part into a third module so both only look at base. This is the most fundamental refactor.
# before: a ↔ b 가 서로의 Base 클래스를 참조
# after — base.py (새로 추출)
class Base:
...
# a.py
from base import Base
class A(Base): ...
# b.py
from base import Base
class B(Base): ...The dependency direction becomes a → base ← b, and the cycle disappears.
Fix 5. Dependency inversion (DI)
Instead of importing a concrete module, inject the object you need as an argument. You cut the direct module-to-module dependency, which also makes testing easier.
# before — service.py
from repository import UserRepository
def get_user(id):
return UserRepository().find(id)# after — service.py
def get_user(id, repo): # repo를 외부에서 주입
return repo.find(id)Comparison table
| Fix | Difficulty | Best when | Downside |
|---|---|---|---|
| Local import inside a function | ★☆☆ Very easy | Quick patch; infrequently called functions | Import lookup on every call; not a root-cause fix |
| Move the import | ★☆☆ Easy | Simple bidirectional case where reordering definitions is enough | Readability suffers as the file grows |
TYPE_CHECKING + strings | ★★☆ Medium | Type-hint-only cycles (the most common) | Cannot use if you actually need the type object at runtime |
| Extract a shared base module | ★★★ Somewhat large | Structural cycle caused by shared code | Broader refactoring scope |
| Dependency inversion (DI) | ★★★ Large | Large projects that value testability | More upfront design and more code |
A practical note: 90% of cases end with Fix 3
In production, most circular imports you hit exist because of a single type-hint line. That's especially true when SQLAlchemy models relate to each other or Pydantic schemas reference each other. When I start a new project I put from __future__ import annotations at the top of every model file by default — that alone kills about 90% of type-hint cycles. A local import inside a function is fast, but it is a "temporary workaround" sticker; once the demo is unblocked, I recommend cleaning up the structure with a base extract or DI.
Conclusion: a decision flowchart and a prevention checklist
Which fix should you pick?
- Is the cycle caused by type hints only? → Fix 3 (
TYPE_CHECKING/from __future__ import annotations) - Is it a simple bidirectional case where reordering definitions is enough? → Fix 2
- Do you just need the build to pass right now? → Fix 1 (temporary), refactor later
- Do the two modules share common code? → Fix 4 (extract base)
- Is this a large codebase where you want to cut the dependency structurally? → Fix 5 (DI)
Circular-import prevention checklist
- Keep module dependencies one-way (higher → lower)
- Avoid indiscriminate re-exports in
__init__.py - Put type-hint-only imports inside a
TYPE_CHECKINGblock - Default to
from __future__ import annotationsat the top of model files - Split shared constants and base classes into a separate
base.py/types.py
References: official docs
The primary source for the behavior, settings, and errors covered in this post is the official documentation below. Check there for version-specific options and exact behavior.
FAQ
Q. What's the difference between ModuleNotFoundError and ImportError: cannot import name?
A. The former means the module file itself was not found (path, install, or typo). The latter means the module was found but a name inside it could not be imported. If cannot import name appears together with partially initialized, a circular import is the cause.
Q. Does adding from __future__ import annotations alone fix every cycle?
A. No. It only fixes cycles caused by type hints. Bidirectional dependencies that import real objects at runtime still need a structural fix: a local import, a base extract, or DI.
Q. Isn't importing inside a function a bad habit? A. There are downsides: an import lookup on every call, and the dependency is no longer visible at the top of the file. It's a solid quick workaround, but in the long run it's better to clean up the structure with a base module or dependency inversion.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.