Как устроены зависимости в Python: venv, pip-tools и контроль повторяемости окружений
Разберём, почему «работает у меня» возникает из-за различий окружений, и как добиться воспроизводимости: lock-файлы, правильная установка и типичные ошибки с версиями.
Содержание
Как устроены зависимости в Python: venv, pip-tools и контроль повторяемости окружений
Проблема “оно работает у меня” в Python — почти всегда не про код. Она про окружение: набор установленных пакетов, их версии, особенности зависимостей и даже то, как именно вы их установили (из PyPI, из локального индекса, с какими опциями, какие ограничения были заданы).
Ситуации знакомы:
- Локально всё проходит тесты, а в CI падает.
- На одной машине проект стартует, на другой — вылезает
ImportErrorили конфликт версий. - Спустя пару недель после “обновления” библиотеки поведение меняется, но в Git вы не видите никаких изменений.
Чтобы это исправить, важно понять, как Python управляет зависимостями: роль venv, поведение pip, что делает pip-tools, и как устроены lock-файлы. Ниже — практический разбор с конкретными командами, типичными ошибками и правилами, которые реально помогают добиться воспроизводимости.
Почему «работает у меня» — это почти всегда про окружение
В Python есть два уровня “контекста”:
- Интерпретатор (версия Python, сборка, платформа).
- Набор зависимостей (версии пакетов и их транзитивные зависимости).
Проблема воспроизводимости начинается, когда разработчики пытаются воспроизвести набор зависимостей косвенно:
- “Установим requirements.txt” — но там либо плавающие версии (
requests>=2.20), либо список “что-то вроде” без фиксации. - “У меня стоит последняя версия пакета” — а у другого человека она может быть другой, потому что
pip installпо умолчанию разрешает зависимости в текущий момент времени. - “Мы обновляли зависимости” — но обновляли без фиксации, или фиксировали частично.
Ключевой момент: если в зависимостях не зафиксированы версии (прямо или через lock-файл), то набор пакетов может меняться со временем, и воспроизводимость исчезает.
Уровень 1: venv и что он реально гарантирует
venv создаёт изолированную среду Python для проекта: отдельные библиотеки, отдельное дерево site-packages, отдельный pip. Это хорошо — но только частично решает проблему.
Что venv гарантирует
- Изоляцию пакетов от системной Python-установки.
- Контроль над тем, какой именно Python-интерпретатор использует проект.
Чего venv не гарантирует
- Он не фиксирует версии пакетов. Вы создаёте пустое окружение, а затем ставите зависимости — и вот тут всё зависит от того, чем вы их описали и как установили.
- Он не защитит от того, что кто-то поставит другую версию пакета, потому что
pipразрешит зависимости иначе.
Минимальный рабочий сценарий
python3 -m venv .venv
source .venv/bin/activate # или .venv\Scripts\activate в Windows
python -m pip install --upgrade pip
В этом месте важно выработать привычку: все команды установки выполняются внутри окружения, а проект всегда запускается из него.
Уровень 2: pip, requirements и главная ловушка
pip работает по запросам, а не по “снимку состояния”. Если вы укажете:
requests>=2.31— тоpipможет поставить любую версию, удовлетворяющую условию, и выбор будет зависеть от момента установки.- “просто установим по списку пакетов” — и получится набор версий, который может отличаться между разработчиками.
Если в requirements.txt лежит только список без фиксации (или с широкими ограничениями), вы не получаете детерминизма.
pip и “разрешение зависимостей”
У pip есть алгоритм разрешения зависимостей: он учитывает Requires-Python, маркеры платформы, и требования транзитивных зависимостей. Но итоговый набор версий выбирается на основе того, какие версии доступны в момент установки.
Отсюда — типичная причина “оно работает у меня”: вы установили окружение в один день, а другой разработчик — в другой, и набор версий слегка сместился.
Воспроизводимость: что нужно зафиксировать
Чтобы окружение было воспроизводимым, нужно фиксировать минимум:
- версии всех прямых зависимостей,
- версии всех транзитивных зависимостей (то есть фактический граф пакетов),
- желательно — ограничения по платформе и Python-версии.
Фиксация прямых зависимостей недостаточна: транзитивные версии всё равно могут “уехать”.
Именно поэтому используют lock-файлы.
pip-tools: зачем он нужен
pip-tools — это инструмент, который помогает отделить:
- описание интентов (какие пакеты нужны и какими ограничениями),
- от генерации конкретного набора версий (lock).
Он оперирует двумя файлами:
requirements.in— “что хотим” (с диапазонами версий, без фиксации всех транзитивных).requirements.txt— “что реально установится” (полный список с точными версиями транзитивных зависимостей).
Ключевая идея
Вы фиксируете результаты разрешения зависимостей один раз — и дальше всем нужно ставить по lock-файлу, а не заново решать граф в каждый момент времени.
Настройка: требования, файлы .in и .txt
Предположим, у проекта есть зависимости:
- веб-фреймворк,
- клиент для внешнего API,
- утилиты для тестов.
1) Создаём requirements.in
fastapi>=0.110,<1.0
httpx>=0.25
pydantic>=2.5
Это намерение: не “поставь ровно такие версии”, а “в рамках допустимого диапазона”.
В отдельности обычно делают requirements-dev.in:
pytest>=7
ruff>=0.3
mypy>=1.8
2) Генерируем lock: requirements.txt
Теперь используем pip-compile:
pip install pip-tools
pip-compile requirements.in --output-file requirements.txt
Результат — файл с точными версиями:
fastapi==0.111.0
httpx==0.27.0
pydantic==2.7.3
...
Пара важных нюансов:
pip-compileвычисляет граф зависимостей и фиксирует все версии.- Это зависит от текущих доступных версий на индексах. Но после генерации lock-файл становится источником правды.
Установка из lock-файла: стабильность вместо догадок
После того как requirements.txt зафиксирован, установка должна быть простой и одинаковой для всех:
pip install -r requirements.txt
На разных машинах, при одинаковом Python-интерпретаторе и одинаковых индексах, вы получите одинаковые версии пакетов.
Обновление зависимостей без потери контроля
Когда приходит время обновлять версии, вы не должны “просто переустановить”. Правильный цикл:
- Обновить ограничения в
.in(например, расширить диапазон или поднять верхнюю границу). - Снова сгенерировать lock:
pip-compile. - Протестировать.
- Закоммитить изменённые
requirements.txt(lock) и.in(намерения).
Пример обновления
pip-compile requirements.in --output-file requirements.txt
После этого diff в Git покажет, какие версии действительно изменились — и вы сможете это обсуждать осознанно.
Контроль воспроизводимости по Python-версии
Есть тонкость: доступные версии пакетов зависят от python_version и маркеров. Поэтому желательно явно зафиксировать диапазон Python, а затем проверять, что lock строится для ожидаемой версии.
Практический подход
- В документации проекта укажите, какую версию Python поддерживаете.
- Сгенерируйте lock в среде нужной версии.
- CI должен использовать тот же Python, что использовался при генерации lock (или близко совместимую).
Если вы сгенерировали lock на Python 3.11, а потом пытаетесь установить на Python 3.9, возможно получение конфликтов или другой набор пакетов.
Разделение окружений: prod, dev, тесты
В реальном проекте вы почти всегда используете несколько наборов зависимостей:
- для production,
- для разработки (линтеры, форматтеры, тестовые инструменты),
- возможно — для разных этапов (например, миграции, сборка доков).
Обычно делают два lock-файла:
requirements.txt(prod),requirements-dev.txt(dev).
Пример генерации dev lock
pip-compile requirements-dev.in --output-file requirements-dev.txt
Установка
pip install -r requirements.txt
pip install -r requirements-dev.txt
Это повышает прозрачность: разработчик понимает, что именно ставится “для работы” и что — “для удобства”.
Важные опции pip-compile: что влияет на повторяемость
У pip-compile есть параметры, которые часто нужны на практике.
Выбор индексов и источников
Если вы используете private index или зеркала PyPI, установки должны быть одинаковыми для генерации lock и последующей установки. Иначе lock может ссылаться на другие версии, чем вы получите при установке.
Hash-checking: детерминизм “на уровне байтов”
Для максимальной строгости можно включить генерацию хэшей:
pip-compile --generate-hashes requirements.in
Тогда в lock-файле будут указаны --hash=... для каждого пакета. При установке pip будет проверять, что скачанные артефакты совпадают с теми, что были зафиксированы.
Это особенно полезно в корпоративных средах и при работе с регламентами по supply-chain security. Но нужно учитывать, что хэши меняются, если пакет переупакован или доступен в другом виде (например, из-за different build).
Типичные ошибки с версиями и lock-файлами
Ниже — то, что чаще всего ломает воспроизводимость даже при наличии pip-tools.
Ошибка 1: не ставить по lock-файлу
Случайный человек может сделать:
pip install -r requirements.in
requirements.in для pip — это тоже “input”, но там могут быть диапазоны. Итог — снова разрешение зависимостей “на лету”.
Правило: ставим по .txt (lock), .in используем только для генерации.
Ошибка 2: игнорировать разницу Python-версий
Если в CI стоит Python 3.10, а локально 3.12, то версии некоторых пакетов могут различаться из-за Requires-Python.
Правило: генерируйте lock на той версии Python, которая используется в целевой среде (или поддерживайте несколько lock для разных мажорных версий, если проект так устроен).
Ошибка 3: смешивать зависимости разработки и production
Когда кто-то включает dev-зависимости в прод lock, вы:
- увеличиваете attack surface,
- усложняете обновления,
- получаете расхождения поведения между окружениями.
Правило: отдельные файлы для prod/dev, отдельные lock.
Ошибка 4: обновлять только .in, но не перегенерировать lock
Вы меняете requirements.in, поднимаете ограничения, но requirements.txt в Git не обновляете. В результате проект использует старый граф зависимостей.
Правило: изменения в .in всегда сопровождаются регенерацией lock и коммитом результата.
Ошибка 5: регенерировать lock без контроля индексов
Если вчера использовался один индекс (например, corporate mirror), а сегодня — прямой pypi.org, итог может отличаться.
Правило: одинаковая конфигурация индексов при генерации и установке. Часто это настраивается через конфиг pip и CI.
Ошибка 6: “частичная воспроизводимость” (фиксировать только прямые зависимости)
Люди иногда делают так:
- фиксируют
requests==..., - но оставляют transitive зависимости в диапазонах через
>=.
В итоге транзитивный набор может меняться.
Правило: фиксировать именно то, что установится — то есть транзитивы тоже. Lock-файл это делает.
Практический workflow: от пустого репозитория до стабильного окружения
Вот рабочий процесс, который можно адаптировать под любую команду.
Шаг 1. Разметить зависимости
requirements.in— prodrequirements-dev.in— dev
Шаг 2. Сгенерировать lock
pip install pip-tools
pip-compile requirements.in --output-file requirements.txt
pip-compile requirements-dev.in --output-file requirements-dev.txt
Шаг 3. Убедиться, что установка детерминирована
В новом окружении (например, на чистой машине) выполнить:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install -r requirements-dev.txt
Проверка: версии должны совпасть с теми, что в lock.
Шаг 4. В CI
- фиксировать Python-версию,
- устанавливать по lock,
- запускать тесты.
Где в этом месте обычно “ломается” производство
Есть несколько неочевидных факторов, которые тоже влияют на поведение.
Сборка wheels и platform-specific пакеты
Некоторые пакеты имеют зависимости на системные библиотеки или используют wheels, зависящие от платформы. На Linux и macOS может быть разный набор wheel, даже при одинаковом lock (хотя версии самих пакетов будут те же).
Если у вас есть такие зависимости — важно тестировать на целевых платформах.
Environment markers
pip может выбирать разные версии одного пакета на разных платформах или при разных условиях Python. Lock-файл часто содержит маркеры, но лучше тестировать фактическую установку в каждой target-среде.
Типовые примеры для конфигураций с extras
Если зависимости включают extras, намерение выглядит так:
requirements.in:
fastapi[standard]>=0.110
pip-compile корректно развернёт extras в транзитивный набор и зафиксирует реальные зависимости. Это ещё один аргумент в пользу генерации lock: вы хотите видеть результат, а не гадать.
Как сделать систему контроля версий “осмысленной”
Чтобы команда не утонула в диффах, стоит договориться:
- lock-файлы коммитятся всегда (иначе воспроизводимость теряется),
- обновления зависимостей делаются отдельными коммитами или PR,
- изменения в
.inбез соответствующих изменений в lock — не принимаются, - желательно добавить в проект короткую документацию “как обновлять зависимости”.
Минимально это может выглядеть так:
requirements.in— намеренияrequirements.txt— lock- обновление: правим
.in→ запускаемpip-compile→ коммитим.txt
Почему это полезно даже тем, кто “и так умеет”
Те, кто работает в одиночку, иногда считают, что им достаточно “следить глазами за версиями” или периодически запускать pip install -U. Это помогает до момента, когда:
- появляется второй разработчик,
- приходит CI,
- вы запускаете проект в Docker,
- вы обновляете пакет, и “вроде всё так же”, но тесты начали падать.
Lock-файл — это не бюрократия. Это механизм фиксации принятого решения по зависимостям, который:
- уменьшает вариативность,
- делает PR по обновлениям предсказуемыми,
- помогает расследовать регрессии.
Вывод: контроль окружений как часть инженерной дисциплины
venv решает проблему изоляции, но не гарантирует повторяемости. pip может переоценить зависимости в момент установки, если вы оставляете диапазоны. Поэтому практический путь к воспроизводимости выглядит так:
- Создаём изолированное окружение (
venv). - Описываем намерения в
.in. - Генерируем lock-файл через
pip-compile. - Ставим зависимости только из lock (
requirements.txt). - Обновляем зависимости контролируемо: правим
.in→ пересчитываем lock → коммитим. - Фиксируем Python-версию в CI и желательно — на этапе генерации lock.
Если вам нужно углубиться именно в механизм работы pip-tools (включая опции, хэши и подходы к multi-platform), полезно пройти структурированный материал, например курс по теме управления зависимостями и воспроизводимыми окружениями: изучить на практике.
Главное — не “собирать окружение каждый раз заново”, а фиксировать результат. Тогда “оно работает у меня” превращается в инженерно доказуемое “оно будет работать так же”.
Комментарии
Пока нет комментариев