یک بار، در پروژه‌ای که از یک کتابخانه‌ی داخلی استفاده می‌کرد، بعد از یک refactor ساده، سرور با خطای عجیبی بالا نیامد: ImportError: cannot import name "get_config" from "app.core". تا آن روز، ImportError را به‌عنوان یک مشکل تایپی می‌شناختم که با بررسی نام ماژول حل می‌شود. آن روز فهمیدم که خطای ImportError در Python، در باطن، یک پنجره به سمت معماری پروژه، مدیریت وابستگی‌ها، و چرخه‌ی حیات ماژول‌ها است. این خطا می‌گوید کد شما انتظار داشته چیزی را از جایی وارد کند که در واقعیت، آن چیز آنجا نیست یا به شکل درستی بارگذاری نشده است.

خطای ImportError در Python دقیقاً چیست؟

Python یک استثنای داخلی به‌نام ImportError دارد که وقتی مطرح می‌شود که کد شما در فرآیند import یک ماژول، کلاس، تابع یا متغیر با مشکل مواجه شود. این خطا یکی از پرتکرارترین خطاهای Python در پروژه‌های واقعی است، چون تقریباً هر پروژه‌ای به وابستگی‌های خارجی و ماژول‌های داخلی متکی است. پیام خطا معمولاً چند شکل دارد:

ImportError: cannot import name "SomeClass" from "some_module"

ImportError: attempted relative import with no known parent package

ImportError: DLL load failed while importing _ssl: The specified module could not be found

هرکدام از این پیام‌ها، نشانه‌ی یک مشکل متفاوت است. پیام اول می‌گوید نام مورد نظر در ماژول وجود ندارد. پیام دوم مربوط به relative imports است. پیام سوم، مشکل سطح بالاتری است که در لایه‌ی کتابخانه‌های بومی (native libraries) رخ می‌دهد.

نکته‌ی مهم این است که ImportError از نوع Exception است و در زمان اجرا رخ می‌دهد. این خطا در زمان parse کد تشخیص داده نمی‌شود، چون Python نمی‌داند که آیا یک ماژول در محیط اجرا وجود دارد یا نه. بنابراین، خطاهای import ممکن است تا زمان اجرا پنهان بمانند.

اگر با مبانی پایتون آشنایی ندارید، ابتدا آموزش پایتون از صفر را بخوانید تا مدل ذهنی درستی از ماژول‌ها و پکیج‌ها شکل بگیرد.

ImportError یک شکایت از پل نیست، یک شکایت از معمار است. Python می‌گوید این وابستگی که در ذهن داشتی، در محیط اجرا به شکل مورد انتظار وجود ندارد. این خطا، یک پنجره به سمت معماری واقعی پروژه است.

مکانیزم import در Python

برای درک درست ImportError، باید مکانیزم import در Python را بشناسیم. Import در Python یک فرآیند چندمرحله‌ای است که با چند فاز اصلی انجام می‌شود:

مرحله اول: جستجو. Python در مسیرهای sys.path به‌دنبال ماژول می‌گردد. این مسیرها شامل پوشه‌ی جاری، مسیرهای نصب پکیج‌ها، و مسیرهای محیطی است.

مرحله دوم: پیدا کردن. وقتی ماژول پیدا شد، Python یک ماژول‌لودر (module loader) مناسب انتخاب می‌کند. لودرهای مختلف برای انواع مختلف ماژول‌ها وجود دارد: SourceFileLoader برای فایل‌های پایتون، ExtensionFileLoader برای افزونه‌های C، SourcelessFileLoader برای فایل‌های کامپایل‌شده.

مرحله سوم: بارگذاری. لودر، کد ماژول را اجرا می‌کند و یک شیء ماژول می‌سازد.

مرحله چهارم: ثبت در sys.modules. ماژول بارگذاری‌شده در sys.modules ثبت می‌شود تا در importهای بعدی، به‌سرعت پیدا شود.

ImportError می‌تواند در هر یک از این مراحل رخ دهد. اگر در مرحله‌ی جستجو رخ دهد، معمولاً ModuleNotFoundError است. اگر در مرحله‌ی بارگذاری رخ دهد، ImportError است. در بخش‌های بعدی، هرکدام را جداگانه بررسی می‌کنم.

تفاوت ImportError و ModuleNotFoundError

در Python 3.6 به بعد، کلاس ModuleNotFoundError معرفی شد که زیرکلاس ImportError است. تفاوت بین این دو مهم است:

ModuleNotFoundError: وقتی رخ می‌دهد که ماژول در هیچ‌کدام از مسیرهای sys.path پیدا نشود. یعنی Python می‌داند که به‌دنبال چه چیزی می‌گردد ولی آن را پیدا نمی‌کند.

import non_existent_module
# ModuleNotFoundError: No module named "non_existent_module"

ImportError: وقتی رخ می‌دهد که ماژول پیدا شده ولی نمی‌توان آن را بارگذاری کرد. مثلاً نام مورد نظر در ماژول وجود ندارد، یا وابستگی داخلی ماژول شکست خورده است.

from os import non_existent_name
# ImportError: cannot import name "non_existent_name" from "os"

این تفکیک، در تشخیص سریع کمک‌کننده است. اگر ModuleNotFoundError می‌گیرید، مشکل در پیدا کردن ماژول است (نصب، مسیر، virtualenv). اگر ImportError می‌گیرید، مشکل در بارگذاری ماژول است (نام، circular import، وابستگی).

مبانی خطاهای مشابه در خطای NameError در پایتون و خطای AttributeError در پایتون آمده است.

شش علت رایج خطای ImportError

در تجربه‌ی من روی صدها پروژه‌ی Python، ImportError از شش علت مشخص می‌آید. شناخت این علت‌ها، تشخیص را در چند دقیقه ممکن می‌کند.

  1. Circular Import: دو ماژول که به هم import می‌زنند.
  2. نام موجود نیست: نام مورد نظر در ماژول تعریف نشده است.
  3. Relative imports نادرست: استفاده از relative import در جایی که context مشخص نیست.
  4. ساختار پکیج نادرست: نبود __init__.py یا ساختار اشتباه.
  5. sys.path اشتباه: مسیرها به‌درستی تنظیم نشده‌اند.
  6. وابستگی نصب نشده: پکیج خارجی در محیط اجرا نصب نیست.

هر علت، نشانه‌های مخصوص به خود و راه‌حل اختصاصی دارد. در بخش‌های بعدی، هر علت را جداگانه باز می‌کنم.

Circular Import: دام ظریف پروژه‌های بزرگ

Circular import یا import دوطرفه، شایع‌ترین و ظریف‌ترین منبع ImportError است. این مشکل وقتی رخ می‌دهد که دو ماژول به هم import می‌زنند:

# module_a.py
from module_b import get_b

def get_a():
    return get_b()

# module_b.py
from module_a import get_a

def get_b():
    return get_a()

وقتی module_a.py اجرا می‌شود، Python در تلاش برای import module_b، به module_a برمی‌گردد و ماژول ناقص است. این مشکل، در Python کلاسیک است و معمولاً با یکی از این سه پیام ظاهر می‌شود:

ImportError: cannot import name "get_a" from partially initialized module "module_a" (most likely due to a circular import)

راه‌حل‌های Circular Import:

راه اول: imports تأخیری (Lazy Imports)

به‌جای import در بالای فایل، import را به داخل تابع منتقل کنید:

# module_a.py
def get_a():
    from module_b import get_b
    return get_b()

# module_b.py
def get_b():
    from module_a import get_a
    return get_a()

این رویکرد، ساده‌ترین و موثرترین راه‌حل است. import فقط زمانی انجام می‌شود که تابع فراخوانی شود، نه در زمان بارگذاری ماژول.

راه دوم: import ماژول (نه نام)

به‌جای import نام مستقیم، ماژول را import کنید:

# module_a.py
import module_b

def get_a():
    return module_b.get_b()

این رویکرد، در بعضی موارد جواب می‌دهد چون Python فقط ماژول را در sys.modules ثبت می‌کند و به‌دنبال نام نمی‌گردد.

راه سوم: بازنویسی معماری

در پروژه‌های بزرگ، بهترین راه‌حل، بازنویسی معماری و جدا کردن مسئولیت‌ها است. مثلاً منطق مشترک را به یک ماژول سوم منتقل کنید که هیچ‌کدام از دو ماژول اصلی به آن وابسته نباشند.

# common.py
def shared_logic():
    pass

# module_a.py
from common import shared_logic

def get_a():
    return shared_logic()

# module_b.py
from common import shared_logic

def get_b():
    return shared_logic()

این رویکرد، تمیزترین و پایدارترین راه‌حل است.

مبانی کار با ماژول‌ها در آموزش پایتون از صفر آمده است.

نام موجود نیست در ماژول

دومین علت شایع ImportError، دسترسی به نامی است که در ماژول وجود ندارد. مثال:

from os import get_current_directory
# ImportError: cannot import name "get_current_directory" from "os"

در این مثال، get_current_directory تابعی است که در os وجود ندارد (تابع درست getcwd است). این خطا در دو حالت رخ می‌دهد:

حالت اول: نام اشتباه است. راه‌حل: مراجعه به مستندات ماژول با dir() یا help():

import os
print(dir(os))         # لیست نام‌های موجود
help(os.getcwd)        # مستندات تابع

حالت دوم: نسخه‌ی ماژول تغییر کرده. نام در نسخه‌ی قدیمی وجود داشته و در نسخه‌ی جدید حذف یا تغییر نام یافته. راه‌حل: بررسی changelog ماژول و به‌روزرسانی کد.

نکته‌ی ظریف: در بعضی موارد، نام مورد نظر در ماژول وجود دارد ولی به‌عنوان صفت قابل دسترسی نیست چون توسط __all__ فیلتر شده یا در __init__.py صادر نشده. راه‌حل: بررسی __all__ در __init__.py پکیج.

Relative و Absolute imports

سومین علت، مربوط به relative imports است. Python از دو نوع import پشتیبانی می‌کند:

Absolute import: import با مسیر کامل از ریشه:

from app.utils.helpers import format_date

Relative import: import نسبت به ماژول فعلی:

from .helpers import format_date    # همان پکیج
from ..utils import format_date     # پکیج والد

Relative imports در Python 3، فقط در داخل پکیج کار می‌کنند. اگر فایلی که relative import دارد، به‌عنوان یک اسکریپت مستقل اجرا شود (نه به‌عنوان بخشی از پکیج)، خطا می‌گیرید:

# app/utils/helpers.py
from . import config
# ImportError: attempted relative import with no known parent package

راه‌حل‌ها:

راه اول: اجرای فایل به‌عنوان بخشی از پکیج

python -m app.utils.helpers

این رویکرد، فایل را به‌عنوان بخشی از پکیج اجرا می‌کند، نه به‌عنوان اسکریپت مستقل.

راه دوم: استفاده از absolute imports

from app.utils import config

absolute imports در همه‌ی شرایط کار می‌کنند، به شرطی که ریشه‌ی پروژه در sys.path باشد.

راه سوم: تنظیم PYTHONPATH

export PYTHONPATH=/path/to/project
python app/utils/helpers.py

این رویکرد، مسیر ریشه‌ی پروژه را در sys.path اضافه می‌کند و absolute imports را ممکن می‌سازد.

__init__.py و ساختار پکیج

چهارمین علت، مربوط به ساختار پکیج و فایل __init__.py است. در Python 3، اگرچه حضور __init__.py برای پکیج‌های معمولی الزامی نیست (namespace packages)، ولی برای پکیج‌های کلاسیک، حضور آن ضروری است.

my_package/
    __init__.py
    module_a.py
    module_b.py
    sub_package/
        __init__.py
        module_c.py

در این ساختار، my_package و sub_package هر دو پکیج هستند. اگر __init__.py در sub_package نباشد، Python آن را به‌عنوان namespace package در نظر می‌گیرد که ممکن است رفتار متفاوتی داشته باشد.

نکته‌ی مهم: در __init__.py، می‌توانید صادرات پکیج را تعریف کنید:

# my_package/__init__.py
from .module_a import some_function
from .module_b import SomeClass

__all__ = ["some_function", "SomeClass"]

با این تعریف، کاربران پکیج می‌توانند مستقیماً از پکیج import کنند:

from my_package import some_function

بدون این تعریف، باید مسیر کامل را طی کنند:

from my_package.module_a import some_function

مبانی ساختار پروژه در مباحث مربوط به توسعه‌ی Python آمده است.

مسائل مسیر و sys.path

پنجمین علت، مربوط به sys.path است. این لیست، مسیرهایی است که Python در آن‌ها به‌دنبال ماژول می‌گردد:

import sys
print(sys.path)

ترتیب مسیرها در sys.path مهم است:

  1. پوشه‌ی اسکریپت اصلی (در صورت اجرای مستقیم)
  2. PYTHONPATH (متغیر محیطی)
  3. مسیرهای نصب پکیج‌های استاندارد
  4. مسیرهای نصب پکیج‌های سایت (site-packages)

مشکلات رایج:

مشکل اول: پروژه در مسیر درست نیست. اگر ریشه‌ی پروژه در sys.path نباشد، imports از پکیج‌های داخلی شکست می‌خورند. راه‌حل: اجرای اسکریپت از ریشه‌ی پروژه، یا تنظیم PYTHONPATH.

مشکل دوم: نام پوشه با پکیج نصب‌شده تداخل دارد. اگر پوشه‌ای با نام json در پروژه‌ی شما وجود داشته باشد، Python آن را به‌جای پکیج استاندارد json import می‌کند. راه‌حل: تغییر نام پوشه یا استفاده از نام‌های متفاوت.

مشکل سوم: در محیط‌های Docker، مسیرهای متفاوت. راه‌حل: تنظیم PYTHONPATH در Dockerfile یا استفاده از مسیرهای نسبی.

virtualenv و مدیریت وابستگی‌ها

ششمین علت، مربوط به virtualenv و مدیریت وابستگی‌ها است. virtualenv یک محیط جداگانه برای هر پروژه است که پکیج‌های آن پروژه را ایزوله می‌کند.

مشکلات رایج:

مشکل اول: virtualenv فعال نیست. اگر virtualenv فعال نباشد، Python از پکیج‌های سراسری استفاده می‌کند. راه‌حل: فعال کردن virtualenv:

# Linux/Mac
source venv/bin/activate

# Windows
venv\Scripts\activate

مشکل دوم: پکیج نصب نشده در virtualenv. اگر پکیج در محیط سراسری نصب شده ولی در virtualenv نه، خطا می‌گیرید. راه‌حل: نصب پکیج در virtualenv فعال:

pip install some_package

مشکل سوم: تفاوت نسخه‌ی Python. اگر virtualenv با Python 3.8 ساخته شده ولی اسکریپت با Python 3.11 اجرا شود، ممکن است مشکلات سازگاری رخ دهد. راه‌حل: تطبیق نسخه:

python3.11 -m venv venv

مشکل چهارم: requirements.txt ناقص. اگر requirements.txt به‌درستی نگهداری نشود، در محیط‌های جدید، پکیج‌های لازم نصب نمی‌شوند. راه‌حل: به‌روزرسانی منظم requirements.txt:

pip freeze > requirements.txt

مبانی virtualenv در مباحث مربوط به توسعه‌ی Python آمده است.

پکیج نصب نشده یا نسخه‌ی اشتباه

گاهی ImportError از این می‌آید که پکیج نصب نشده یا نسخه‌ی نصب‌شده متفاوت از نسخه‌ی مورد انتظار است.

نشانه‌ها:

  • ModuleNotFoundError: No module named "requests"
  • ImportError: cannot import name "X" from "Y" وقتی X در نسخه‌ی نصب‌شده وجود ندارد.

راه‌حل‌ها:

راه اول: نصب پکیج

pip install requests

راه دوم: نصب نسخه‌ی مشخص

pip install requests==2.28.0

راه سوم: بررسی نسخه‌ی نصب‌شده

pip show requests
pip list

راه چهارم: به‌روزرسانی pip

pip install --upgrade pip

نکته‌ی ظریف: در پروژه‌های بزرگ، استفاده از requirements.txt یا pyproject.toml برای مدیریت نسخه‌ها ضروری است. این ابزارها، نسخه‌های دقیق را ثبت می‌کنند و از مشکلات سازگاری پیشگیری می‌کنند. مبانی کامل در مباحث مربوط به توسعه‌ی Python آمده است.

Conditional imports و platform-specific

در بعضی موارد، ImportError از این می‌آید که کد شما یک ماژول را به‌طور مشروط import می‌کند، ولی شرط به‌درستی تنظیم نشده:

if sys.platform == "win32":
    import winreg  # فقط روی Windows موجود است

# روی Linux، خطا می‌گیرید اگر این import اجرا شود

راه‌حل: استفاده از try/except برای conditional imports:

try:
    import winreg
    HAS_WINREG = True
except ImportError:
    HAS_WINREG = False

این رویکرد، استاندارد در کتابخانه‌هایی است که باید روی چند پلتفرم کار کنند. مثال: کتابخانه‌های pathlib، os، و subprocess از این الگو استفاده می‌کنند.

ImportError در Django و Flask

در فریم‌ورک‌های وب Python، ImportError از منابع خاص خود می‌آید:

در Django

# settings.py
INSTALLED_APPS = [
    "myapp",  # ImportError اگر myapp در sys.path نباشد
]

راه‌حل: بررسی نام app و اطمینان از حضور پوشه‌ی آن در ریشه‌ی پروژه.

# models.py
from myapp.utils import something  # ImportError اگر circular باشد

راه‌حل: استفاده از imports تأخیری یا بازنویسی معماری. مبانی کامل در آموزش جنگو برای مبتدیان و مباحث امنیتی در بهترین روش‌های امنیت Django آمده است.

در Flask

# app.py
from myapp import create_app  # ImportError اگر ساختار پکیج نادرست باشد

راه‌حل: اطمینان از حضور __init__.py در myapp و تنظیم صادرات در آن. مبانی کامل در آموزش فلسک در پایتون آمده است.

در FastAPI

from pydantic import BaseModel  # ModuleNotFoundError اگر pydantic نصب نباشد

راه‌حل: نصب pydantic با pip install pydantic. مبانی FastAPI در مباحث مربوط به API آمده است.

روش تشخیص اصولی در چهار گام

در تجربه‌ی من، تشخیص ImportError در چند دقیقه انجام می‌شود، اگر روش سیستماتیک داشته باشید:

گام اول: خواندن دقیق پیام خطا. پیام ImportError معمولاً دقیقاً می‌گوید کدام ماژول و کدام نام. این دو داده، جهت جستجو را تعیین می‌کند.

گام دوم: بررسی وجود ماژول. اگر ModuleNotFoundError است، اول بررسی کنید که ماژول نصب است:

pip show some_package
python -c "import some_package; print(some_package.__file__)"

گام سوم: بررسی مسیر و ساختار. اگر ماژول نصب است ولی پیدا نمی‌شود، مسیرهای sys.path را بررسی کنید:

import sys
for p in sys.path:
    print(p)

گام چهارم: بررسی Circular Import. اگر پیام خطا به «partially initialized module» اشاره می‌کند، احتمالاً circular import است. با تحلیل گراف وابستگی‌ها، می‌توانید مسیر circular را پیدا کنید:

pip install pydeps
pydeps my_package --show-dot

ابزارهای تشخیص:

  • python -c "import module": تست مستقیم import.
  • pip show package_name: بررسی نصب پکیج.
  • pip list: لیست پکیج‌های نصب‌شده.
  • pipdeptree: نمایش گراف وابستگی‌ها.
  • pydeps: نمایش گراف ماژول‌های پروژه.

مبانی کامل مدیریت خطا در مدیریت خطا در پایتون آمده است.

راهبردهای رفع اصولی

بعد از تشخیص، نوبت به رفع می‌رسد. راهبردهای رفع، بر اساس نوع خطا متفاوت است:

راهبرد اول: imports تأخیری

برای circular import، imports را به داخل تابع منتقل کنید:

def process_data():
    from .helpers import transform
    return transform(data)

این رویکرد، ساده‌ترین راه‌حل است و در ۷۰ درصد موارد circular import جواب می‌دهد.

راهبرد دوم: absolute imports

به‌جای relative imports، از absolute imports استفاده کنید:

# به‌جای
from .utils import helper

# از
from my_package.utils import helper

این رویکرد، خواناتر و پایدارتر است و در محیط‌های مختلف کار می‌کند.

راهبرد سوم: تنظیم PYTHONPATH

اگر پروژه در مسیر درستی نیست، PYTHONPATH را تنظیم کنید:

export PYTHONPATH=/path/to/project:$PYTHONPATH

در فایل‌های .env یا docker-compose.yml، این متغیر را تنظیم کنید.

راهبرد چهارم: استفاده از pyproject.toml

در پروژه‌های مدرن، از pyproject.toml برای مدیریت پکیج استفاده کنید:

[project]
name = "my_project"
version = "1.0.0"
dependencies = [
    "requests>=2.28.0",
    "pydantic>=2.0.0",
]

سپس با pip install -e . پروژه را نصب کنید. این رویکرد، مدیریت وابستگی‌ها را ساده‌تر می‌کند.

راهبرد پنجم: بازنویسی معماری

در پروژه‌های بزرگ، بهترین راه‌حل، بازنویسی معماری است. معماری‌های پیشنهادی:

  • Layered Architecture: جدا کردن لایه‌ها (models، services، controllers).
  • Hexagonal Architecture: جدا کردن منطق دامنه از وابستگی‌های خارجی.
  • Dependency Injection: تزریق وابستگی‌ها به‌جای import مستقیم.

این رویکردها، از circular import پیشگیری می‌کنند و پروژه را قابل نگهداری‌تر می‌کنند.

راه‌حل‌های Circular Import

Circular import یکی از مزمن‌ترین مشکلات در پروژه‌های Python است. در تجربه‌ی من، چند راه‌حل اصولی وجود دارد:

راه‌حل اول: Type Hints و TYPE_CHECKING

اگر circular import فقط برای type hints است، از TYPE_CHECKING استفاده کنید:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from module_b import SomeClass

def process(obj: "SomeClass") -> None:
    pass

این رویکرد، در زمان اجرا import را انجام نمی‌دهد، ولی در IDE و mypy، type hints کار می‌کنند.

راه‌حل دوم: Function-Level Imports

import را به داخل تابع منتقل کنید:

def process_data():
    from .helpers import transform
    return transform(data)

راه‌حل سوم: Shared Module

منطق مشترک را به یک ماژول سوم منتقل کنید که هیچ‌کدام از دو ماژول اصلی به آن وابسته نباشند:

# shared.py
def shared_logic():
    pass

# module_a.py
from shared import shared_logic

# module_b.py
from shared import shared_logic

راه‌حل چهارم: Late Binding

در مواردی که وابستگی دوطرفه منطقی است، از late binding استفاده کنید:

# module_a.py
def get_a():
    from module_b import get_b
    return get_b()

# module_b.py
import module_a

def get_b():
    return module_a.get_a()

در این الگو، module_a به‌طور کامل بارگذاری می‌شود و بعد تابع get_b فراخوانی می‌شود.

مبانی کار با ماژول‌ها در مباحث Python آمده است. ابزارهای تحلیل وابستگی در مباحث ابزارهای توسعه توضیح داده شده است.

ImportError در محیط production

در محیط production، ImportError ابعاد جدی‌تری دارد:

قطع کامل سرویس

اگر ImportError در زمان startup برنامه رخ دهد، کل سرویس بالا نمی‌آید. این حالت، معمولاً بعد از deployment یا بعد از نصب یک وابستگی جدید رخ می‌دهد.

نشت اطلاعات

پیام خطا، مسیر فایل‌ها و نام پکیج‌ها را افشا می‌کند. راه‌حل: در production، لاگ‌ها را در جای امن نگه دارید و پیام عمومی به کاربر نشان دهید.

پایش و آلارم‌دهی

در production، ImportError باید به‌طور مناسب پایش شود. ابزارهایی مثل Sentry و Rollbar، این خطاها را جمع‌بندی می‌کنند. نکته: خطاهای import معمولاً در زمان startup رخ می‌دهند، بنابراین پایش باید از همان ابتدا فعال باشد.

پیشگیری با تست

بیشتر این خطاها را می‌توان قبل از production با تست‌های خودکار کشف کرد:

  • Import tests: تست import همه‌ی ماژول‌ها در CI.
  • Dependency check: تست نصب همه‌ی وابستگی‌ها از requirements.txt.
  • Smoke tests: تست startup برنامه در محیط شبیه‌سازی‌شده.

مبانی تست در مباحث Python آمده است.

اشتباهات رایج در برخورد با ImportError

در طول سال‌ها، الگوهای تکراری از اشتباهات دیده‌ام که هر کدام می‌تواند پروژه را به چالش بکشد:

اشتباه اول: catch کردن عام ImportError

استفاده از except ImportError: pass برای سرکوب خطا، مشکل را پنهان می‌کند. راه‌حل: خطا را در لاگ ثبت کنید یا با پیام واضح مطرح کنید.

اشتباه دوم: نصب پکیج در محیط سراسری

نصب پکیج در محیط سراسری به‌جای virtualenv، منجر به تداخل نسخه‌ها می‌شود. راه‌حل: همیشه از virtualenv استفاده کنید.

اشتباه سوم: نادیده گرفتن warning در import

اگر در فرآیند import، warning ظاهر شود ولی خطا نباشد، توسعه‌دهنده‌ها اغلب آن را نادیده می‌گیرند. در حالی که warning می‌تواند نشانه‌ی مشکل جدی‌تری باشد. راه‌حل: همیشه warningها را در لاگ بررسی کنید.

اشتباه چهارم: استفاده از from module import *

این رویکرد، namespace را آلوده می‌کند و می‌تواند به circular import یا تداخل نام کمک کند. راه‌حل: همیشه imports صریح استفاده کنید.

اشتباه پنجم: نبود تست import در CI

اگر CI تست import را اجرا نکند، خطاهای import در production کشف می‌شوند. راه‌حل: در CI، یک اسکریپت ساده بنویسید که همه‌ی ماژول‌ها را import کند.

اشتباه ششم: نادیده گرفتن تفاوت محیط‌ها

کد در لوکال کار می‌کند ولی در production خطا می‌دهد. ریشه: تفاوت نسخه‌ی پکیج‌ها، نسخه‌ی Python، و محیط. راه‌حل: استفاده از Docker یا محیط‌های ایزوله با تنظیمات مشابه.

اشتباه هفتم: عدم توجه به ترتیب importها

در بعضی موارد، ترتیب importها مهم است. اگر ماژول A را قبل از B import کنید، ممکن است B در حالت ناقص باشد. راه‌حل: بررسی وابستگی‌ها و تنظیم ترتیب درست.

ImportError یک پیام از معماری است، نه از کد. Python می‌گوید این وابستگی که در ذهن داشتی، در محیط اجرا به شکل مورد انتظار وجود ندارد. راه‌حل، بازنویسی معماری است نه سرکوب خطا.

پرسش‌های پرتکرار درباره خطای ImportError در پایتون

این پرسش‌ها از دل تجربه‌ی عملی و جلسات مشاوره جمع‌آوری شده‌اند. پاسخ هر کدام بر اساس سناریوهای واقعی است.

تفاوت ImportError و ModuleNotFoundError چیست؟

ModuleNotFoundError یک زیرکلاس ImportError است که به‌طور خاص وقتی رخ می‌دهد که ماژول پیدا نشود. ImportError عمومی‌تر است و شامل خطاهای دیگر مثل نبود نام در ماژول یا خطاهای circular import می‌شود. بنابراین، هر ModuleNotFoundError یک ImportError هم هست، ولی برعکسش نه.

چرا این خطا در پروژه‌های بزرگ‌تر شایع‌تر است؟

چون در پروژه‌های بزرگ، تعداد ماژول‌ها و وابستگی‌ها بیشتر است و احتمال circular import بالا می‌رود. راه‌حل: معماری لایه‌ای، جدا کردن مسئولیت‌ها، و استفاده از absolute imports.

آیا می‌توانم از try/except برای ImportError استفاده کنم؟

بله، در موارد خاص مثل conditional imports یا fallback پکیج‌ها، این رویکرد قابل توجیه است. ولی نباید به‌عنوان راه‌حل عمومی برای پنهان کردن خطا استفاده شود. الگوی درست:

try:
    import ujson as json  # سریع‌تر
except ImportError:
    import json  # fallback

آیا circular import همیشه مشکل است؟

نه همیشه. در بعضی موارد، circular import با imports تأخیری یا TYPE_CHECKING قابل مدیریت است. مشکل زمانی است که circular import در زمان اجرا باعث خطا شود.

چگونه circular import را تشخیص دهم؟

پیام خطا معمولاً به «partially initialized module» اشاره می‌کند. برای تحلیل دقیق‌تر، از ابزارهایی مثل pydeps استفاده کنید که گراف وابستگی‌ها را نمایش می‌دهد. در پروژه‌های بزرگ، این گراف می‌تواند چرخه‌ها را مشخص کند.

آیا virtualenv می‌تواند مشکل ImportError را حل کند؟

virtualenv بخش بزرگی از مشکلات ModuleNotFoundError را حل می‌کند چون محیط ایزوله فراهم می‌کند. ولی برای ImportError ناشی از circular import، virtualenv کمکی نمی‌کند.

چگونه در Docker، ImportError را مدیریت کنم؟

سه رویکرد: اول، تنظیم PYTHONPATH در Dockerfile. دوم، استفاده از WORKDIR مناسب. سوم، نصب پروژه به‌عنوان پکیج با pip install -e .. مثال:

FROM python:3.11
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
ENV PYTHONPATH=/app
CMD ["python", "-m", "app.main"]

آیا pip install -e . می‌تواند ImportError را حل کند؟

در بسیاری از موارد بله. pip install -e . پروژه را در حالت editable نصب می‌کند و پکیج را در site-packages ثبت می‌کند. این رویکرد، برای پروژه‌های Python استاندارد است.

چرا بعد از آپدیت پکیج، ImportError ظاهر می‌شود؟

چون پکیج‌ها ممکن است API را در نسخه‌های جدید تغییر دهند. راه‌حل: changelog پکیج را مرور کنید و کد را با API جدید تطبیق دهید. همچنین، در requirements.txt، نسخه‌های دقیق را ثبت کنید تا از آپدیت‌های ناخواسته جلوگیری شود.

آیا Python 3.11 راه‌حل خاصی برای ImportError دارد؟

Python 3.11 پیام‌های خطا را بهبود داده است. مخصوصاً در مورد circular import، پیام دقیق‌تری نمایش می‌دهد. همچنین، performance import بهبود یافته که در پروژه‌های بزرگ اهمیت دارد.

چگونه از ImportError در پکیج‌های سفارشی پیشگیری کنم؟

سه رویکرد: اول، تست خودکار import همه‌ی ماژول‌ها در CI. دوم، استفاده از pyproject.toml برای تعریف ساختار پکیج. سوم، مستندسازی وابستگی‌ها و imports در README.

آیا ImportError روی performance تأثیر دارد؟

خود ImportError در لحظه‌ی وقوع رخ می‌دهد. اگر مدیریت شود، از نظر performance هزینه‌ی ناچیزی دارد. ولی import ماژول‌های سنگین (مثل Pandas یا TensorFlow) می‌تواند زمان startup را افزایش دهد. راه‌حل: از imports تأخیری برای ماژول‌های سنگین استفاده کنید.

چگونه در pytest، ImportErrorها را تست کنم؟

با pytest.raises(ImportError) یا pytest.raises(ModuleNotFoundError):

import pytest

def test_missing_module():
    with pytest.raises(ModuleNotFoundError):
        import non_existent_module

تفاوت from module import * و from module import name چیست؟

from module import * تمام نام‌های موجود در __all__ ماژول (یا تمام نام‌های عمومی در صورت نبود __all__) را وارد می‌کند. این رویکرد، namespace را آلوده می‌کند و به circular import کمک می‌کند. from module import name فقط نام مشخص را وارد می‌کند و خواناتر است.

چرا در Jupyter Notebook، ImportError شایع است؟

چون در Jupyter، kernel یک محیط جداگانه است و ممکن است پکیج‌های نصب‌شده در سیستم، در kernel نصب نباشند. راه‌حل: نصب پکیج در kernel با !pip install package_name یا بررسی با !pip list.

چگونه در FastAPI، ImportError را مدیریت کنم؟

FastAPI از pydantic و starlette استفاده می‌کند. رایج‌ترین ImportError زمانی است که پکیج نصب نباشد. راه‌حل: نصب کامل با pip install fastapi[all] و استفاده از requirements.txt.

آیا می‌توانم circular import را با importlib حل کنم؟

بله، در مواردی که imports پیچیده است:

import importlib

def get_helper():
    module = importlib.import_module("my_module.helpers")
    return module.helper_function()

این رویکرد، import را به زمان اجرا موکول می‌کند و در بعضی موارد از circular جلوگیری می‌کند.

آیا ImportError می‌تواند ناشی از فایل‌های خراب باشد؟

بله، در مواردی که فایل پایتون ناقص یا خراب باشد، ممکن است در هنگام import، خطاهای دیگری رخ دهد که به شکل ImportError ظاهر شوند. راه‌حل: بررسی فایل با python -m py_compile module.py.

تفاوت ImportError و SyntaxError در فایل‌های import‌شده چیست؟

SyntaxError در زمان parse فایل رخ می‌دهد و اگر در فایل import‌شده باشد، به‌عنوان خطای همان فایل نمایش داده می‌شود. ImportError در زمان بارگذاری ماژول رخ می‌دهد. تفاوت: SyntaxError از نظر نگارشی است، ImportError از نظر ساختاری.

چگونه در asyncio، ImportError را مدیریت کنم؟

در asyncio، خطاها در event loop مدیریت می‌شوند. راه‌حل: مدیریت صریح ImportError در coroutineها و استفاده از imports تأخیری برای ماژول‌های پرهزینه.

چرا در Windows، ImportError بیشتر دیده می‌شود؟

در Windows، مسائل مربوط به DLLها می‌تواند باعث ImportError شود. مثال کلاسیک: ImportError: DLL load failed while importing _ssl که ناشی از نبود Microsoft Visual C++ Redistributable است. راه‌حل: نصب بسته‌های لازم سیستم.

چگونه در پکیج‌های Python، صادرات را تعریف کنم؟

در __init__.py:

from .module_a import some_function
from .module_b import SomeClass

__all__ = ["some_function", "SomeClass"]

این رویکرد، صادرات پکیج را مشخص می‌کند و کاربران می‌توانند مستقیماً از پکیج import کنند. مبانی ساختار پکیج در مباحث Python آمده است.

آیا ImportError می‌تواند به دلیل encoding فایل باشد؟

غیرمستقیم، بله. اگر فایل با encoding اشتباهی ذخیره شود (مثلاً UTF-8 with BOM)، ممکن است در هنگام import خطاهای عجیبی رخ دهد. راه‌حل: ذخیره فایل‌ها با UTF-8 بدون BOM.

آنچه از سال‌ها کار با ImportError در Python آموختم

اگر بخواهم چکیده‌ی این سال‌ها را در چند جمله بگویم، سه اصل عملی دارم:

یک: imports تأخیری، ابزار کارآمد برای circular import است. در ۷۰ درصد موارد، انتقال import به داخل تابع، مشکل circular را حل می‌کند. برای موارد پیچیده‌تر، بازنویسی معماری ضروری است.

دو: absolute imports، خواناتر و پایدارتر است. در پروژه‌های مدرن، absolute imports را به relative imports ترجیح دهید. این رویکرد، در محیط‌های مختلف کار می‌کند و خطاهای مسیر را کاهش می‌دهد.

سه: تست import در CI، سرمایه‌گذاری بلندمدت است. یک اسکریپت ساده که همه‌ی ماژول‌ها را import کند، می‌تواند بخش بزرگی از ImportErrorها را قبل از deployment کشف کند. این رویکرد در پروژه‌های بزرگ، حیاتی است.

در کنار این سه اصل، یک هشدار عملی هم دارم: ImportError در نگاه اول یک مشکل ساده به‌نظر می‌رسد، ولی وقتی در چارچوب کلی معماری پروژه دیده شود، تبدیل به یک سیگنال می‌شود. این سیگنال می‌گوید که معماری پروژه‌ی شما نیاز به بازنگری دارد. اگر این سیگنال را جدی بگیرید و ساختار را بهبود دهید، پروژه‌ی شما در ماه‌های بعد پایدارتر و قابل نگهداری‌تر خواهد بود.

هدف این مقاله، تمام‌کردن همه‌ی سناریوهای ممکن نبود. هدف، دادن یک چارچوب ذهنی برای تشخیص، پیشگیری و رفع این خطا بود. وقتی این چارچوب را درونی کنید، برخورد با ImportError از یک واکنش اضطراری به یک فرآیند منظم تبدیل می‌شود.

اگر ImportError در پروژه‌ی شما به شکلی ظاهر شده که با الگوهای این مقاله حل نشده، برای من جالب است بدانم کدام سناریو بود. تجربه‌ی خودتان را در دیدگاه‌ها بنویسید؛ به‌ویژه اگر راه‌حلی پیدا کرده‌اید که هنوز در این مقاله نیست. 🔗