Анатомия 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.pyModuleNotFoundError: 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в коде — симптом кривой структуры.Циклический импорт лечится структурой, а не импортом внутри функции.