Проект, который два года спокойно жил на одной машине, часто ломается при первой попытке развернуть его на другой. Разработчик клонирует репозиторий, запускает установку зависимостей, ждёт пару минут и получает вместо работающего приложения гору красных тестов и трассировку с ImportError. В requirements.txt тем временем всё выглядит прилично: знакомые названия пакетов, местами значок "не ниже такой-то версии". Файл не врёт, он просто умалчивает. За два года вокруг десятка верхнеуровневых строчек вырос целый куст транзитивных зависимостей, и установщик на чистой машине собирает уже не тот набор, который был рабочим в день релиза. Ниже разбор того, как эту проблему решают Poetry и uv, чем они различаются на долгоживущем проекте и сколько стоит привычка писать названия пакетов без версий.

Как незакреплённые версии в requirements.txt ломают проект к его второму году

Типичный requirements.txt пишется в день рождения проекта, а вспоминают о нём обычно уже при поломке:

fastapi
uvicorn>=0.20
requests
pandas
sqlalchemy

Запись без версии означает "ставь любую подходящую", запись со знаком "не ниже" чуть честнее, но смысл тот же: решение о составе окружения принимает не автор проекта, а установщик, и не в день написания, а в момент установки. На машине, где проект создавался, всё продолжало работать, ведь туда два года назад встали конкретные версии, и с тех пор их никто не трогал. Чистая машина запускает поиск свежих релизов, и набор пакетов начинает расползаться.

За два года экосистема успевает встряхнуть проект несколько раз. В апреле 2023 года вышел urllib3 2.0 с несовместимыми изменениями, и код, рассчитанный на первую версию, местами перестал работать. Летом 2023 года вышла вторая Pydantic с переписанным API, старые модели потребовали миграции. В июне 2024 года релиз numpy 2.0 удалил десятки давно осуждённых псевдонимов вроде np.float_, и связки со старым pandas посыпались с малопонятными ошибками типов. Тогда же pip 24.1 ужесточил проверку описаний пакетов, и библиотеки, годами спокойно ставившиеся, начали отказывать с сообщением invalid metadata. В октябре 2024 года завершилась поддержка Python 3.8, осенью 2025 года то же случилось с 3.9, а свежие дистрибутивы Linux по правилу PEP 668 вообще не дают pip ставить пакеты в системное окружение.

Каждое из этих событий сообщество переживало за считанные недели. Проект с незакреплёнными версиями получает весь набор проблем разом и именно в момент развёртывания, когда до релиза остаётся час. Простейшее лекарство, снимок pip freeze, действительно спасает: в файле оказываются точные версии всего установленного. Лекарство грубое, потому что снимок не отличает верхнеуровневые пакеты от транзитивных. Через два года в файле из восьмидесяти строк уже не понять, что из этого нужно самому приложению, а что приехало по чужой зависимости и давно никем не используется.

Как Poetry держит окружение через pyproject.toml и poetry.lock

Poetry появился в феврале 2018 года и первым из массовых инструментов приучил сообщество к схеме из двух файлов. В pyproject.toml человек описывает намерение: какие пакеты нужны и в каких границах версий. В poetry.lock машина фиксирует фактическое состояние: точные версии, хеши архивов, источники. Замок пересчитывается только по явной команде и хранит внутри контрольную сумму pyproject.toml, так что рассинхрон между намерением и фактом обнаруживается мгновенно.

Базовый цикл выглядит так:

poetry init
poetry add "uvicorn[standard]"
poetry lock
poetry install --sync

Флаг --sync не просто доустанавливает недостающее, а удаляет из окружения лишнее, доводя его до состояния замка. Для легаси-конвейеров, которые понимают только requirements.txt, есть экспорт с хешами:

poetry export --output requirements.txt --with-hashes

Сильные стороны накопились за годы: плагины, приватные индексы, привычки тысяч команд. Версия 2.0, вышедшая в январе 2025 года, перешла на стандартную секцию project.dependencies по PEP 621, так что pyproject.toml, собранный Poetry, перестал быть экзотикой для чужих инструментов. Дальше релизы пошли ровным ритмом: от 2.1 в феврале 2025 года до 2.5 в сентябре 2026 года.

Слабое место известно давно: скорость резолвера. На бенчмарке с зависимостями библиотеки Trio, который приводила сама Astral, Poetry решал граф за 0.60 секунды на тёплом кэше, а установка занимала 1.90 секунды. Для редких обновлений разница неощутима. Для непрерывной интеграции, где окружение пересобирается десятки раз в день, счёт уже выходит заметный.

uv от Astral и его реальная скорость на цифрах установки пакетов

15 февраля 2024 года компания Astral, известная линтером Ruff, выпустила первую версию uv. Заявка была простой: экстремально быстрый установщик и резолвер на Rust, совместимый по командам с pip и pip-tools. Цифры за словами стояли крепкие: без кэша uv обходил pip и pip-tools в 8-10 раз, на тёплом кэше преимущество доходило до 80-115 раз. На том же бенчмарке с зависимостями Trio разрешение графа заняло у uv 0.01 секунды против 1.56 секунды у pip-compile, установка 0.06 секунды против 0.99 секунды у pip-sync и 4.63 секунды у pdm.

Секрет скорости прозаичен: глобальный кэш пакетов, жёсткие ссылки и копирование при записи вместо лишних распаковок, один статический бинарник без зависимости от системного питона. Скачанный однажды пакет больше не качается заново, а окружения собираются из кэша почти мгновенно. Совместимость с pip не потерялась: встроенный интерфейс uv pip принимает старые команды вроде uv pip install и uv pip compile, так что в проект можно заходить постепенно.

За два года uv из быстрой замены pip вырос в универсальный менеджер. Встроенное управление версиями интерпретатора: команда uv python install скачивает готовые сборки CPython, отдельный менеджер версий больше не нужен. Команда uv tool и сокращение uvx заменили pipx для установки утилит вроде ruff. Появились рабочие пространства для монорепозиториев, команда uv run для запуска в проектном окружении, а с версии 0.7 в мае 2025 года ещё и сборка с публикацией пакетов через uv build и uv publish. Сегодня инструмент живёт в ветке 0.13.

Плата за скорость была одна: молодость. В 2024 году сценариев и чужого опыта у uv было заметно меньше, чем у Poetry, и осторожные команды выжидали. К 2026 году вопрос зрелости почти снят.

Чем различаются lock-файлы Poetry и uv при переносе проекта между платформами

poetry.lock хранит точные версии и хеши для окружения, в котором замок создавался. Операционная система и версия интерпретатора участвуют в выборе готовых сборок, поэтому при переносе с ноутбука разработчика в Linux-контейнер Poetry иногда просит пересчитать замок. uv.lock устроен иначе: он изначально универсален и описывает граф сразу для Windows, macOS и Linux, для разных версий питона и архитектур процессоров. Один и тот же замок честно собирается у всех участников команды.

За универсальность платят размером: uv.lock разрастается в тысячи строк, в диффах он шумный, и это нормально, ведь замок читает машина, а не человек.

Оба замка хранят хеши архивов, и это не формальность. При установке каждый архив сверяется с хешем из замка, подменить пакет посреди цепочки доставки не выйдет. Свежий релиз с вредоносным содержимым тоже не пройдёт: версия в замке зафиксирована, хеш зафиксирован, случайная новинка в окружение просто не попадёт.

Мосты между инструментами на месте: requirements.txt импортируется одной командой, а замок экспортируется обратно в классический вид.

uv add -r requirements.txt
uv sync --locked
uv export --format requirements.txt

Вредоносные релизы в PyPI и цена привычки ставить всё самое свежее

В апреле 2026 года администраторы PyPI опубликовали разбор инцидента с популярными пакетами litellm и telnyx. Сначала через скомпрометированную зависимость в сервисе сканирования контейнеров Trivy утекли токены доступа, затем с их помощью в индекс выложили вредоносные версии двух библиотек. Код срабатывал прямо при установке, собирал ключи и файлы с машины и отправлял их на внешний адрес. Отравлённые версии litellm успели скачать 119 000 раз, прежде чем их изолировали. Реакция была быстрой: от загрузки до первого сообщения о вредоносном коде прошло 1 час 19 минут, ещё через 1 час 12 минут пакеты отправились в карантин. Итого 2 часа 32 минуты экспозиции.

Час с небольшим кажется коротким отрезком, пока не взглянуть на масштаб. litellm ставят 15-20 миллионов раз в неделю, около 1700 установок в минуту. По оценкам администраторов, от 40 до 50 процентов этих установок шло без закрепления версий, то есть тянуло самый свежий релиз автоматически. Именно такие установки и получали вредоносный код первыми. Те, кто работал по замку с зафиксированной версией и хешами, продолжали спокойно качать предыдущий чистый релиз.

Поток, за которым надо следить, огромен: в PyPI ежедневно появляется 700-800 новых проектов. Функция жалоб на вредоносные пакеты, добавленная в 2024 году, ускорила реакцию администрации, но работает она уже после факта. В том же разборе администраторы прямо рекомендуют закреплять зависимости и выдерживать паузу перед внедрением чужих свежих релизов. Замок с хешами защищает дважды: непроверенная версия не попадёт в окружение, потому что зафиксирована, и подменённый архив не пройдёт, потому что хеш не сойдётся.

Пошаговый план вывода живого проекта из хаоса незакреплённых зависимостей

Миграция старого проекта на Poetry или uv занимает вечер, если держаться порядка и не пытаться переехать за один вдох. Проверенная последовательность выглядит так:

  1. Снять снимок работающего окружения командой pip freeze и сохранить его отдельным файлом как временную точку отсчёта;
  2. Прогнать на этом снимке полный набор тестов и записать базовое время сборки, чтобы сравнивать результаты числами, а не ощущениями;
  3. Перенести в новый инструмент только верхнеуровневые зависимости вручную, а транзитивные пусть вычислит сам инструмент;
  4. Сгенерировать lock-файл заново и добиться зелёных тестов на чистой машине, собранной с нуля;
  5. Добавить в непрерывную интеграцию установку строго по lock-файлу с запретом пересчёта на месте;
  6. Настроить регулярный аудит уязвимостей и превратить обновления зависимостей из разовой акции в плановый процесс с паузой перед внедрением.

Для маршрута через uv команды минимальны:

pip freeze > requirements.lock.txt
uv add -r requirements.txt
uv sync --locked

Для маршрута через Poetry цикл знакомый:

poetry init
poetry add fastapi
poetry lock
poetry install --sync

Если с первого раза граф не собирается, почти всегда виноват один пакет с жёсткой верхней границей версии. Лечится точечно: границу ослабляют, конфликтный пакет обновляют, тесты подтверждают решение. Главное, снимок до миграции: он позволяет вернуться и сравнить поведение до и после.

Когда переезд с Poetry на uv оправдан, а когда он только добавляет работы

Poetry живёт в продакшене с 2018 года, и его сильные стороны за это время не изменились: зрелый резолвер, плагины, приватные индексы, огромный запас сценариев и чужого опыта в сообществе. Если команда раз в неделю обновляет зависимости на двадцати проектах и никому не мешает минута ожидания, переезд не окупится. Разница между установкой за 1.9 секунды и за 0.06 секунды на редких операциях неощутима, а перестройка привычек стоит реальных часов.

Переезд оправдан там, где установки идут потоком. Непрерывная интеграция с десятками сборок в день умножает сэкономленные секунды в заметные часы. Монорепозиторий с общими зависимостями получает рабочие пространства. Командам, которые держат несколько версий интерпретатора, uv закрывает вопрос одним инструментом: uv python install, uv run, uv sync. Частые развёртывания в контейнеры получают от тёплого кэша максимальную выгоду.

Выбор не на всю жизнь: оба инструмента говорят на PEP 621, переезд касается в основном замка и привычек, и многие команды спокойно держат Poetry для старых сервисов, а uv для новых.

Инструмент в этой истории вторичен, первична дисциплина. Замок хранится в репозитории, обновления идут осознанными маленькими шагами, тесты запускаются после каждого шага, перед чужим свежим релизом выдерживается пауза. requirements.txt без версий честно обещает "поставим что-нибудь подходящее". Через два года это "что-нибудь" превращается в чужую проблему, которую по странной традиции разгребает тот, кто меньше всех виноват.