Программирование с ИИ
ENRU
6.1

Анатомия Python-проекта

пакеты, импорты и почему файл не запускается

Что это

Статья 2.3 разбирала устройство проекта вообще. Здесь — то, что устроено по-питоновски: пакеты, __init__.py, импорты и запуск через python -m.

Тема выглядит скучной ровно до первого ModuleNotFoundError в проекте, который вчера работал. Девять из десяти таких ошибок — отсюда.

Как выглядит в коде

измерено
weatherbot/
├── __init__.py          делает папку пакетом
├── __main__.py          что выполнится при python -m weatherbot
├── config.py
└── handlers/
    ├── __init__.py
    └── weather.py
# weatherbot/handlers/weather.py
from weatherbot.config import MAX_TEXT


def cmd_weather(city: str) -> str:
    return f"погода для {city} (лимит {MAX_TEXT})"
# weatherbot/__main__.py
from weatherbot.handlers.weather import cmd_weather


def main() -> None:
    print(cmd_weather("Oslo"))


if __name__ == "__main__":
    main()

Главная ловушка

Запускаем файл напрямую, как привыкли:

python3 weatherbot/handlers/weather.py
измерено
ModuleNotFoundError: No module named 'weatherbot'

Код правильный, импорт правильный, файл на месте — и не работает.

Причина в том, откуда Python начинает искать модули. При запуске файла он ищет от папки этого файла, то есть от handlers/. Пакета weatherbot там нет — он уровнем выше.

Правильный запуск — из корня проекта, как модуль:

python3 -m weatherbot
измерено
погода для Oslo (лимит 200)

Теперь поиск начинается от текущей папки, weatherbot находится, импорты работают.

Что такое __init__.py

Файл, который говорит Python: «эта папка — пакет, из неё можно импортировать». Чаще всего пустой, и это нормально.

Если он непустой — загляни: код внутри выполняется при первом обращении к пакету. Туда иногда складывают импорты «для удобства»:

# weatherbot/handlers/__init__.py
from .weather import cmd_weather     # теперь можно from weatherbot.handlers import cmd_weather

Удобно, но это ещё одно место, где код выполняется неожиданно (статья 2.2).

Два вида импортов

from weatherbot.config import MAX_TEXT    # абсолютный
from .config import MAX_TEXT              # относительный
from ..config import MAX_TEXT             # на уровень выше

Точка означает «от текущего пакета». Относительные импорты короче, но у них есть неприятное свойство: файл с ними нельзя запустить напрямую вообще никогда.

Практический совет: в своих проектах используй абсолютные. Они длиннее, зато работают одинаково везде и читаются без подсчёта точек.

Что ещё лежит в корне

ФайлЗачем
requirements.txtзависимости (статья 5.6)
pyproject.tomlсовременная замена: зависимости, настройки инструментов
.envсекреты, не в git (5.7)
.gitignoreчто не коммитить
tests/тесты (4.6)

pyproject.toml всё чаще заменяет requirements.txt. Если в проекте есть оба — смотри, какой реально используется при установке, иначе окажется, что версии разъехались.

Как сказать ИИ

Запрос
Покажи дерево файлов проекта без .venv и __pycache__.
Для каждой папки — одна строка, что в ней.
Запрос
Не запускается: ModuleNotFoundError: No module named 'weatherbot'.
Структура проекта такая: <дерево>. Запускаю так: <команда>.
Как запускать правильно?

При создании нового проекта стоит сразу задать структуру:

Запрос
Создай скелет проекта: пакет weatherbot с подпакетом handlers,
точка входа через python -m, абсолютные импорты.
Файлы пустые, только структура и __init__.py.

На что смотреть

Относительные импорты вперемешку с абсолютными. В одном файле from .config, в другом from weatherbot.config — работать будет, читаться плохо.

sys.path.append в коде. Признак того, что структура проекта неправильная, а проблему залатали вручную. Почти всегда лечится правильным запуском.

Код в __init__.py. Выполняется при импорте пакета, и об этом легко забыть.

Циклические импорты. a.py импортирует b.py, который импортирует a.py. Python падает с ImportError: cannot import name. ИИ любит чинить это импортом внутри функции — работает, но причина остаётся: структура запутана.

Файл на тысячу строк в корне. Проект не разделён, и любая правка задевает всё остальное.

Шпаргалка

  • __init__.py делает папку пакетом; обычно пустой, но может выполняться.

  • ModuleNotFoundError на свой пакет — неверный запуск, а не ошибка кода.

  • Запускай python -m пакет из корня проекта.

  • Абсолютные импорты предсказуемее относительных.

  • sys.path.append в коде — симптом кривой структуры.

  • Циклический импорт лечится структурой, а не импортом внутри функции.