Sửa lỗi ImportError: cannot import name từ partially initialized module (circular import) trong Python

intermediate🐍 Python2026-03-22| Python 3.x, tất cả hệ điều hành (Linux, macOS, Windows), bất kỳ dự án nào sử dụng nhiều module

Error Message

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

Lỗi xuất hiện ngay khi khởi động

Ứng dụng của bạn đang chạy tốt. Bạn thêm một import mới, và bây giờ nó không thể khởi động:

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

Câu "most likely due to a circular import" là Python đang nói lịch sự. Đó chính xác là circular import. Module A import từ module B, và module B lại import ngược lại từ module A. Python bắt đầu tải A, gặp lệnh import B, bắt đầu tải B, lại gặp lệnh import A, và nhận được một module chưa hoàn chỉnh. Bất kỳ tên nào bạn cố import đều chưa tồn tại.

Tìm vòng lặp trước

Trước khi chỉnh sửa bất cứ thứ gì, hãy đọc toàn bộ traceback — không chỉ dòng cuối:

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'

Đọc từ dưới lên. models.py import từ services.py, nhưng services.py đang trong quá trình tải dở khi Python đến đây. Vòng lặp tìm thấy: services → models → services.

Với các dự án lớn hơn khi chuỗi không rõ ràng, thêm một dòng debug tạm thời ở đầu các module nghi ngờ:

# Debug tạm thời — xóa sau khi tìm ra vòng lặp
import sys
print(f"Loading {__name__}, already loaded: {[m for m in sys.modules if 'app' in m]}")

Chạy entry point và quan sát thứ tự in ra. Vòng lặp sẽ lộ ra trong vài dòng.

Ba cách phá vỡ vòng lặp

Cách 1: Chuyển import vào trong hàm

Cách sửa nhanh nhất. Thay vì import ở cấp module, hãy import bên trong hàm thực sự cần dùng nó:

# services.py — TRƯỚC (gây circular import)
from app.models import User

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

# services.py — SAU (deferred import)
class UserService:
    def get_user(self, user_id):
        from app.models import User  # Import ở đây, không phải ở cấp module
        return User.query.get(user_id)

Python chỉ thực thi lệnh import khi get_user() được gọi, lúc đó cả hai module đã được tải hoàn toàn. Đây không phải pattern sạch nhất, nhưng nó rõ ràng và hoạt động ngay.

Cách 2: Tách code dùng chung ra module thứ ba

Cách sửa kiến trúc đúng đắn. Khi A và B import lẫn nhau, hầu như chúng luôn chia sẻ một khái niệm nên thuộc về module riêng. Tách nó ra:

# Trước:
# models.py import từ services.py
# services.py import từ models.py

# Sau: tạo app/base.py hoặc app/types.py
# base.py
from dataclasses import dataclass

@dataclass
class UserData:
    id: int
    email: str

# models.py
from app.base import UserData  # Không cần import services nữa

# services.py
from app.base import UserData  # Không cần import models nữa

Circular import là dấu hiệu cảnh báo. Hai module đó đang liên kết quá chặt chẽ, và việc tách ra này sửa thiết kế gốc, không chỉ là triệu chứng.

Cách 3: Dùng TYPE_CHECKING cho type hints

Nhiều circular import trong Python hiện đại tồn tại hoàn toàn chỉ cho type annotation — import đó không bao giờ cần thiết lúc runtime. Bảo vệ nó với TYPE_CHECKING:

from __future__ import annotations  # Biến tất cả annotation thành string lúc runtime
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from app.services import UserService  # Chỉ import khi type checking

class User:
    def get_service(self) -> 'UserService':  # Hoạt động bình thường
        ...

TYPE_CHECKING luôn là False lúc runtime — mypy và pyright coi nó là True khi phân tích tĩnh. Không có circular import lúc runtime, type safety đầy đủ lúc kiểm tra.

Pattern TYPE_CHECKING đầy đủ

Đáng để xem ví dụ trước/sau hoàn chỉnh, vì pattern này loại bỏ được một số lượng đáng ngạc nhiên các vòng lặp thực tế:

# Chưa sửa — 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]:
        ...
# Đã sửa
# 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 giờ là 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]:  # Hoạt động
        ...

Kiểm tra lại sau khi sửa

Đừng chỉ khởi động lại và hi vọng. Kiểm tra từng module riêng lẻ từ thư mục gốc dự án:

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')"

Sau đó chạy toàn bộ test suite:

python -m pytest tests/ -x  # -x dừng lại ở lỗi đầu tiên

Để tự động phát hiện import cycles, pylint có tính năng phát hiện vòng lặp tích hợp sẵn:

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

Ngăn không để lỗi quay lại

Circular import thường lén lút quay lại trong các team lớn. Một vài thói quen giúp kiểm soát điều này:

  • Phân lớp import: chọn một hướng phụ thuộc (ví dụ: routes → services → models → base) và không bao giờ import ngược chiều
  • Thêm phát hiện vòng lặp vào CI: chạy pylint --enable=R0401 trong pipeline để vòng lặp bị phát hiện trước khi merge
  • Giữ __init__.py gọn nhẹ: re-export tất cả từ __init__.py của một package là nguồn gốc phổ biến nhất của các vòng lặp ẩn
  • Mặc định dùng TYPE_CHECKING cho annotation — thêm vào checklist code review của bạn

Vẫn còn lỗi? Kiểm tra cache .pyc

Nếu vòng lặp đã được gỡ nhưng lỗi vẫn còn, bytecode cũ có thể đang phát lại import graph cũ. Xóa nó đi:

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

Chín trong mười trường hợp, một trong ba cách trên sẽ giúp bạn vượt qua lỗi này mà không cần chạm vào logic cốt lõi.

Related Error Notes