Сообщить об ошибке.

Стандарт импортов и запуск Python-проектов

Содержание:

Стандарт импортов в Python: пакет приложения внутри корня проекта, запуск через python -m или entrypoints, абсолютные импорты от имени пакета, минимум PYTHONPATH, отсутствие правок sys.path в коде. Надёжно в dev, CI и проде. И при смене CWD Всегда.

Базовые определения

Корень проекта (<project_root>) - условная папка, где лежат код и окружение разработки/деплоя: зависимости, конфиги, скрипты, инфраструктурные файлы.Это удобный термин для людей. Для Python это не специальная сущность.

Пакет - папка с Python-модулями, обычно с __init__.py.Имя пакета появляется только тогда, когда папка пакета находится внутри одной из директорий из sys.path.

Главное правило импортов:Чтобы работало:

from app.config import SETTINGS

должно существовать:

<dir in sys.path>/
  app/
    __init__.py
    config.py

Почему ломается "всё работало вчера"

Большинство поломок возникает из-за зависимости импортов от:

  1. CWD (текущей директории запуска).
  2. Случайного PYTHONPATH в окружении.
  3. Ручного вмешательства в sys.path в коде.
  4. Запуска файла как "голого скрипта" вместо пакетного запуска.

Золотой стандарт (если нужно, чтобы не ломалось)

Этот вариант является отраслевой нормой для устойчивых проектов.Если критична стабильность импортов и отсутствие сюрпризов при деплое, он не обсуждается.

Каноничная структура

/opt/example_service/              <project_root>
  pyproject.toml (или requirements.txt)
  .venv/
  deploy/
  scripts/
  app/                              <import_root package>
    __init__.py
    config.py
    telemetry/
      __init__.py
      agent.py
    clone/
      __init__.py
      tools/
        __init__.py
        helpers.py

Каноничные импорты

Абсолютные импорты от имени пакета:

from app.config import SETTINGS
from app.telemetry.agent import main
from app.clone.tools.helpers import prepare_name

Почему именно так:

  • пространство имён однозначно;
  • минимальный риск коллизий (config, tools, utils больше не "болтаются" глобально);
  • одинаковое поведение локально, в CI и в проде.

Каноничный запуск

Запуск как модуль

Из корня проекта:

cd /opt/example_service
/opt/example_service/.venv/bin/python -m app.telemetry.agent

Почему это надёжно:

  • Python корректно понимает пакетный контекст;
  • относительные импорты внутри пакета работают предсказуемо;
  • меньше зависимости от окружения.

Entry points (лучший вариант эксплуатации)

В pyproject.toml:

[project.scripts]
example-agent = "app.telemetry.agent:main"

Запуск:

example-agent

Плюсы:

  • единый интерфейс запуска;
  • минимум ошибок при операционной поддержке.

Конфигурация Supervisor в золотом стандарте

[program:example-agent]
directory=/opt/example_service
environment=PYTHONUNBUFFERED=1
command=/opt/example_service/.venv/bin/python -m app.telemetry.agent

Смысл:

  • directory фиксирует рабочую директорию;
  • запуск через -m фиксирует пакетную семантику;
  • PYTHONPATH не нужен как обязательное условие.

Разбор спорных узлов

"Импорты от корня" вроде service.config

Это не импорты от "корня проекта".Это импорты от пакета с именем service.

Чтобы работало:

from service.config import X

нужна структура:

<project_root>/
  service/
    __init__.py
    config.py

То есть папка service должна быть отдельным пакетом внутри корня проекта.

Почему не помогает __init__.py прямо в <project_root>

Вариант:

/var/example_service/
  __init__.py
  config.py

Интуитивно кажется, что теперь пакет называется example_service. Но Python так не думает.

Чтобы импортировать "пакет, равный корню проекта", нужен родитель корня проекта в sys.path.

Например:

  • пакет = /var/example_service
  • значит в sys.path должен быть /var

В реальности при запуске из корня:

cd /var/example_service
python ...

в sys.path окажется /var/example_service, а не /var.Поэтому:

import example_service

не станет стабильно работать только из-за __init__.py в корне.

Вывод: Добавление __init__.py в корень проекта - не архитектурное решение.

Почему "/var/service" не меняет сути

Путь в файловой системе (/srv, /opt, /var, /home) не влияет на модель импортов.

Важна только логика:

<dir in sys.path>/
  <package_name>/

Если проект лежит в:

/var/service/
  service/
    __init__.py

то это корректный вариант для server.*. Если лежит так:

/var/service/
  __init__.py

то ожидаемого import service не будет без нестандартного sys.path.

Допустимые компромиссы (но не стандарт)

Иногда нужно быстро и без перестройки структуры.

PYTHONPATH как эксплуатационная подпорка

Пример:

/opt/example_service/
  telemetry/agent.py
  conf.py

Если файл из подпапки импортирует модуль из корня:

import conf

Тогда рабочий вариант окружения:

  • directory=<project_root>
  • PYTHONPATH=<project_root>

Это соответствует ранее обсуждённой логике: PYTHONPATH добавляет корень проекта в sys.path, и модуль из корня становится доступным.

Но:

  • это зависимость от окружения;
  • при запуске вне Supervisor легко забыть переменную.

Жёсткие антипаттерны

Эти пункты почти гарантируют будущие проблемы:

  1. Правка sys.path в коде
import sys
sys.path.insert(0, "..")
  1. Импорты "как будто пакет глобальный", когда он вложенный
from tools.helpers import X # при фактической структуре app/clone/tools
  1. Запуск внутренних модулей напрямую файлом, если проект уже пакетный:
python app/telemetry/agent.py

вместо:

python -m app.telemetry.agent

Контрольный чек-лист устойчивости

  1. Внутри <project_root> есть ровно один явный пакет приложения:
    app/
    
  2. Везде используются абсолютные импорты:
    from app.... import ...
    
  3. Запуск только так:
    python -m app....
    
    • или через entry points.
  4. PYTHONPATH - не обязательное условие работоспособности.
  5. В коде отсутствуют изменения sys.path.

Итоговая формула

Если цель - чтобы всё работало и не ломалось при любой смене CWD, способа запуска и окружения, применяется один устойчивый подход:

  • отдельный пакет приложения внутри корня проекта,
  • абсолютные импорты от имени пакета,
  • запуск через python -m или entry points,
  • никаких правок sys.path в коде,
  • PYTHONPATH только как редкий временный компромисс.

Это и есть золотой стандарт для долгоживущих Python-сервисов.

Мини-стандарт для серверного Python-проекта

Структура

<project_root>/
  .venv/
  example_service/
    __init__.py
    config.py
    telemetry/
      __init__.py
      agent_spool_ship.py
    tasks/
      __init__.py
      worker.py
    clone/
      __init__.py
      lib/
        __init__.py
      template/
        __init__.py

Импорты

Только так:

from example_service.config import ...
from example_service.telemetry.agent_spool_ship import ...

Запуск локально

cd <project_root>
.venv/bin/python3 -m example_service.telemetry.agent_spool_ship
.venv/bin/python3 -m example_service.tasks.worker

Supervisor

directory=<project_root>
environment=PYTHONUNBUFFERED=1
command=<project_root>/.venv/bin/python3 -m example_service.telemetry.agent_spool_ship

Установка зависимостей

<project_root>/.venv/bin/python3 -m pip install -r requirements.txt
# или
<project_root>/.venv/bin/python3 -m pip install -e .

3 "золотых" правила на память

  1. Импорт example_service.* возможен только если существует папка example_service/ как пакет.
  2. В sys.path должен быть родитель пакета (обычно <project_root>).
  3. Самый устойчивый запуск - python -m example_service....