/개발/Python ImportError: cannot import name 순환 참조 해결 5가지
개발Python 순환참조ImportError cannot import name

Python ImportError: cannot import name 순환 참조 해결 5가지

ImportError: cannot import name ... from partially initialized module 에러는 순환 참조가 원인입니다. 에러 메시지 읽는 법부터 TYPE_CHECKING·지역 import·DI까지 before/after 복붙 코드로 정리했습니다.

Python ImportError: cannot import name 순환 참조 해결 5가지

Python ImportError: cannot import name 순환 참조 완벽 해결 가이드

이 글은 ModuleNotFoundError와 다릅니다. ModuleNotFoundError: No module named 'models'는 모듈 파일 자체를 못 찾는 문제(경로·설치·오타)입니다. 이 글이 다루는 건 모듈은 분명히 찾았는데 그 안의 '이름'을 못 가져오는 순환 참조(circular import) 문제입니다. (모듈을 못 찾는 경우라면 → ModuleNotFoundError 해결 가이드 글을 참고하세요.)

"분명 함수 이름 맞는데 왜 import가 안 돼?"

분명히 models.pyUser 클래스가 있고, 철자도 맞는데 이런 에러가 뜹니다.

CODE
ImportError: cannot import name 'User' from partially initialized module 'models' (most likely due to a circular import) (/app/models.py)

이 에러를 처음 보면 "내가 클래스 이름을 잘못 썼나?" 하고 models.py를 몇 번이고 다시 열어보게 됩니다. 하지만 진짜 단서는 괄호 안에 이미 적혀 있습니다.

(most likely due to a circular import) — 이 한 줄이 핵심입니다.

Python이 친절하게 "이거 순환 참조일 확률이 높아요"라고 알려주고 있는 겁니다. 그리고 partially initialized module(절반만 초기화된 모듈)이라는 표현이 무슨 일이 벌어졌는지 정확히 말해줍니다. 에러 메시지 한 줄만 제대로 읽으면 어디가 문제인지 보입니다.

에러 메시지 한 줄 해부: 왜 "partially initialized"인가

Python은 모듈을 import할 때 위에서 아래로 코드를 한 번 실행하면서 그 안의 클래스·함수를 메모리에 등록합니다. 문제는 이 실행이 중간에 다른 모듈을 import하면 거기서 잠깐 멈춘다는 점입니다.

a.pyb.py를 부르고, b.py가 다시 a.py를 부르는 상황을 단계별로 따라가 봅시다.

CODE
① 누군가 import a  →  a.py 실행 시작
② a.py 1번째 줄에서 import b  →  a 실행을 멈추고 b.py로 점프
③ b.py가 from a import something 실행
   →  a는 아직 ②단계, 즉 something 정의 줄까지 못 갔음!
   →  a는 "절반만 초기화된(partially initialized)" 상태
④ 그래서 cannot import name 'something' from partially initialized module 'a'

즉, 이름이 없는 게 아니라 아직 정의되기 전에 가져가려 한 것입니다. 타이밍 문제죠. 이 그림이 머릿속에 박히면 나머지는 쉽습니다.

Python 3.12+부터는 import 트레이스백이 더 친절해져서 어떤 줄에서 순환이 시작됐는지 추적하기가 한결 수월해졌습니다. 트레이스백의 가장 안쪽 프레임을 보면 "어느 모듈이 누구를 부르다가 막혔는지"가 드러납니다.

순환 참조가 생기는 전형적 구조 3가지

① 모듈 A ↔ B 양방향 import

가장 흔한 케이스입니다. 복붙해서 직접 재현해보세요.

Python
# a.py
from b import func_b

def func_a():
    return "a"

print(func_b())
Python
# b.py
from a import func_a   # 💥 a는 아직 func_a 정의 전

def func_b():
    return "b"

python a.py 실행 → cannot import name 'func_a' from partially initialized module 'a'.

__init__.py의 과도한 re-export

패키지를 깔끔하게 쓰려고 __init__.py에서 전부 끌어모으다 보면 순환이 생깁니다. FastAPI·SQLAlchemy 프로젝트에서 단골입니다.

Python
# models/__init__.py
from .user import User
from .order import Order   # order.py가 다시 models의 User를 import하면 순환
Python
# models/order.py
from models import User    # 💥 __init__.py가 아직 실행 중

③ 타입 힌트 때문에 끌려오는 import

런타임에는 전혀 필요 없는데, 오직 타입 힌트를 적으려고 import했다가 순환이 나는 경우입니다. Pydantic·SQLAlchemy 모델 사이에서 자주 발생합니다.

Python
# user.py
from order import Order   # 오직 아래 타입힌트용

class User:
    def latest_order(self) -> Order:   # 사실 런타임엔 Order 객체 필요 없음
        ...
Python
# order.py
from user import User     # 💥 user ↔ order 순환

class Order:
    owner: User

해결법 5가지 (before / after 복붙 코드)

해결 1. 함수 내부 지역 import

import를 모듈 최상단이 아니라 실제로 쓰는 함수 안으로 내립니다. 함수가 호출되는 시점엔 양쪽 모듈이 이미 초기화돼 있으니 안전합니다.

Python
# before — b.py
from a import func_a

def func_b():
    return func_a()
Python
# after — b.py
def func_b():
    from a import func_a   # 호출 시점에 import → 순환 회피
    return func_a()

해결 2. import 위치를 파일 끝으로 옮기기

a.py가 필요한 정의를 모두 마친 뒤에 import하면 절반 초기화 문제를 피할 수 있습니다.

Python
# after — a.py
def func_a():
    return "a"

from b import func_b   # 정의가 끝난 뒤 import
print(func_b())

해결 3. TYPE_CHECKING + 문자열 어노테이션 (타입힌트 순환의 정석)

구조 ③처럼 타입 힌트 때문에 생긴 순환이라면 이게 정답입니다. TYPE_CHECKING은 런타임엔 False라서 실제 import는 일어나지 않고, 타입 체커(mypy·Pyright)와 IDE만 인식합니다.

Python
# before — user.py
from order import Order

class User:
    def latest_order(self) -> Order:
        ...
Python
# after — user.py
from typing import TYPE_CHECKING

if TYPE_CHECKING:        # 런타임엔 실행 안 됨 → 순환 끊김
    from order import Order

class User:
    def latest_order(self) -> "Order":   # 문자열 어노테이션
        ...

더 간단하게, 파일 맨 위에 한 줄만 추가하는 방법도 있습니다. PEP 563 덕분에 모든 어노테이션이 자동으로 문자열 취급되어, 따옴표 없이도 동작합니다.

Python
# 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
        ...

해결 4. 공통 모듈(base)로 추출

A와 B가 서로 공유하는 코드가 있어 순환이 생긴다면, 그 공통 부분을 제3의 모듈로 빼서 둘 다 base만 바라보게 만듭니다. 가장 근본적인 리팩터링입니다.

Python
# 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): ...

의존 방향이 a → base ← b로 바뀌어 순환이 사라집니다.

해결 5. 의존성 역전(DI)

구체 모듈을 직접 import하는 대신, 필요한 객체를 인자로 주입받습니다. 모듈 간 직접 의존 자체를 끊는 방식이라 테스트도 쉬워집니다.

Python
# before — service.py
from repository import UserRepository

def get_user(id):
    return UserRepository().find(id)
Python
# after — service.py
def get_user(id, repo):       # repo를 외부에서 주입
    return repo.find(id)

해결법 비교 표

해결법적용 난이도권장 상황단점
함수 내부 지역 import★☆☆ 매우 쉬움급한 임시 처방, 호출 빈도 낮은 함수호출마다 import 조회, 근본 해결 아님
import 위치 변경★☆☆ 쉬움정의 순서만 바꾸면 되는 단순 양방향파일이 커지면 가독성 저하
TYPE_CHECKING+문자열★★☆ 보통타입힌트 전용 순환 (가장 흔함)런타임에 타입 객체가 진짜 필요하면 불가
공통 base 모듈 분리★★★ 다소 큼공유 코드로 구조적 순환 발생리팩터링 범위가 넓음
의존성 역전(DI)★★★ 큼대형 프로젝트, 테스트 용이성 중시초기 설계·코드량 증가

실무 한마디: 90%는 해결 3으로 끝납니다

현업에서 마주치는 순환 참조의 체감상 대부분은 타입 힌트 한 줄 때문에 생깁니다. SQLAlchemy 모델끼리 관계를 맺거나 Pydantic 스키마가 서로를 참조할 때가 특히 그렇죠. 저는 새 프로젝트를 시작하면 모델 파일 맨 위에 from __future__ import annotations를 기본으로 깔아두는데, 이것만으로 타입힌트발 순환의 9할이 사라집니다. 함수 내부 지역 import는 빠르지만 "임시방편"이라는 표시를 남기는 셈이니, 데모를 막은 뒤엔 base 분리나 DI로 구조를 정리해두길 권합니다.

결론: 상황별 선택 플로차트 & 예방 체크리스트

어떤 해결법을 고를까?

  1. 순환의 원인이 타입 힌트뿐인가? → 해결 3 (TYPE_CHECKING / from __future__ import annotations)
  2. 정의 순서만 바꾸면 되는 단순 양방향인가? → 해결 2
  3. 지금 당장 빌드만 통과하면 되나? → 해결 1(임시), 나중에 리팩터링
  4. 두 모듈이 공통 코드를 공유하나? → 해결 4(base 분리)
  5. 구조적으로 의존을 끊고 싶은 대형 코드베이스인가? → 해결 5(DI)

순환 참조 예방 체크리스트

  • 모듈 의존 방향을 한 방향(상위→하위)으로 유지한다
  • __init__.py에서 무분별한 re-export를 피한다
  • 타입 힌트 전용 import는 TYPE_CHECKING 블록 안에 둔다
  • 모델 파일 상단에 from __future__ import annotations를 기본 적용한다
  • 공유 상수·base 클래스는 별도 base.py/types.py로 분리한다

자주 묻는 질문 (FAQ)

Q. ModuleNotFoundErrorImportError: cannot import name은 뭐가 다른가요? A. 전자는 모듈 파일 자체를 못 찾은 것(경로·설치·오타 문제)이고, 후자는 모듈은 찾았지만 그 안의 이름을 못 가져온 것입니다. cannot import namepartially initialized가 함께 보이면 순환 참조가 원인입니다.

Q. from __future__ import annotations 한 줄만 넣으면 모든 순환이 풀리나요? A. 아니요. 타입 힌트 때문에 생긴 순환만 해결됩니다. 런타임에 실제 객체를 import해 쓰는 양방향 의존은 지역 import, base 분리, DI 같은 구조적 해결이 필요합니다.

Q. 함수 안에 import를 넣는 건 나쁜 습관 아닌가요? A. 매 호출마다 import 조회 비용이 생기고 의존 관계가 코드 상단에서 안 보인다는 단점은 있습니다. 빠른 회피책으로는 훌륭하지만, 장기적으로는 base 모듈 분리나 의존성 역전으로 구조 자체를 정리하는 편이 좋습니다.

✦ ✦ ✦
편집 검토 · Editorial Review

이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.

작성 · Content Reviewer·검토 · 사람 편집자·발행 · 2026년 6월 16일

댓글

불러오는 중...