Быстрее, выше, сильнее: сравнение подходов poetry, rye и uv

Привет, с вами снова Егор, Tech Lead компании ИдаПроджект. Я все еще занимаюсь стратегией, процессами и командами в направлении backend-разработки :)

Когда-то давно (по меркам IT), шесть лет назад, мы сходили на конференцию и послушали про poetry, преисполнились и внедрили его у себя на проектах. Но ничто не стоит на месте: вот уже два года мир знает о uv, а недавно появился еще и rye. Поэтому я посвятил пару выходных тестированию этих инструментов, чтобы использовать на наших типичных проектах.

В этой статье сравним poetry, uv и rye: кто быстрее управляет зависимостями, как использовать их в Docker, и какой выбрать в 2025 году. Заодно пробежимся по философии инструментов и посмотрим пару новых PEP стандартов, которые могут улучшить работу с зависимостями.

Оглавление

Что такое rye и его философия

Что такое poetry и какие проблемы он решил в 2019

И какой же poetry в 2025?

→ Наш типичный проект с Poetry, и как он собирается в Docker

Метрики по сборке: скорость и потребление ресурсов

Переведем проект для использования uv

Метрики по сборке

Отличие lock файла в poetry и в uv

И напоследок rye…

Заключение

Что такое rye и его философия

В текущей реализации (на момент написания статьи актуальная версия 0.43.0) rye — это экспериментальное решение для сборки и управления локальными проектами. К нему стоит относиться как к попытке привести python-разработку к единому стандарту сборки. Кроме того, rye замахивается на внедрение единого стандарта форматирования кода, линтинг и вообще — переизобретение стандарта PEP 8. 

В релизной форме rye хочет стать прослойкой, через которую будет идти вся разработка под python.

Чтобы понять, для чего создали rye, и какие проблемы хотели решить с его помощью, почитайте документ о его философии. Автор rye, Armin Ronacher (создал и поддерживает проекты Pallets, куда входит, например, всем известный Flask) рассказывает о сложностях в разработке и ссылается на язык rust и его инструменты (rustup и cargo) как на цель, к которой должна стремиться python-разработка. 

Общие цели проекта можно свести к следующим пунктам:

  • Сделать единый механизм получения и установки интерпретатора python для разработки и эксплуатации. Это формат реализации стандарта PEP 771.

  • Использовать строгие правила по управлению зависимостями и прийти к semver как к стандарту.

  • Кеширование метаданных пакетов для оптимизаций сборки приложения.

  • Сделать стандарт Lockfiles (сейчас уже есть драфт PEP 751) и ускорить процесс разрешения зависимостей. Сейчас под капотом используется uv.

  • Стандартизировать использование virtualenv.

  • Стандартизировать использование внешних инструментов и инструментария во время разработки.

Планы действительно впечатляющее, но пока посмотрим, как rye «ляжет» на наш стандартный проект, и оценим, насколько удобнее работать с ним, а не с poetry (сейчас он используется у нас в компании по стандарту).

Однако я не буду затрагивать базовое использование rye, почитайте об этом в обзорной статье.

Что такое poetry и какие проблемы он решил в 2019

До Poetry управление зависимостями в Python было болью. Конфликты версий, ручная правка requirements.txt — знакомо? Poetry это изменил. 

Pyproject.toml стал центром проекта, где описаны все зависимости. poetry.lock фиксировал версии всех пакетов, включая подзависимости. Больше не нужно гадать с версиями и вручную перебирать их для сборки проекта. Poetry дал надежный способ управлять зависимостями и гарантировать воспроизводимость сборок. Requirements.txt ушел на второй план для управления проектами, уступив место современному подходу с lock-файлами.

Появилось «центральное окно управления проектом» в виде pyproject.toml, где определяются не только зависимости проекта, но и dev-инструментарий, дополнительные скрипты для сборки, параметры линтинга и форматирования кода. Poetry сам автоматически создает нужные виртуальные окружения, что позволило в некоторых кейсах перейти из Docker-контейнеров обратно в разработку с использованием автоматически генерируемых virtualenv окружений. Также это позволило упростить подключение интерпретатора к IDE без костылей: через создание стандартного виртуального окружения в папке проекта. IDE могут автоматически обнаруживать и использовать это окружение, что упрощает настройку интерпретатора и отладчика. Это контрастирует с ситуацией, когда виртуальные окружения создавались «как попало», или вообще использовался системный интерпретатор, что влекло за собой проблемы с настройкой IDE и переходом между проектами.

Если кратко, то poetry дал понять, что можно многое поменять и сделать проще, если стандартизировать все аспекты локальной разработки через единый интерфейс. 

И какой же poetry в 2025?

Сейчас poetry представляет собой инструментарий для полноценной работы с проектом и закрывает почти все потребности маленьких и средних команд. В текущих экспериментальных релизах poetry добавилось управление установкой python, и улучшилась производительность фиксирования зависимостей. 

Наш типичный проект с Poetry, и как он собирается в Docker

Для базового примера возьмем маленький проект — с минимальным количеством нужных зависимостей. 

[tool.poetry.dependencies]

# core

python = "3.12.*"

Django = "4.2.*"

psycopg2-binary = "2.9.*"

django-redis = "5.4.*"

gunicorn = "^20.1.0"

# Models \ Admin

phonenumbers = "^8.12.29"

django-phonenumber-field = "5.2.*"

# Rest

djangorestframework = "3.14.*"

drf-spectacular = "0.24.*"

django-filter = "22.*"

# Test

factory-boy = "^3.2.0"

pytest = "^6.2.4"

pytest-mock = "^3.6.1"

pytest-cov = "^2.12.1"

pytest-django = "^4.4.0"

pytest-sugar = "^0.9.4"

# Others

Pillow = "10.0.1"

[poetry.group.dev.dependencies]

django-debug-toolbar = "^3.2.1"

# linting

ruff = "0.0.*"

# Typehinting

mypy = "1.*"

mypy-extensions = "1.*"

django-stubs = "1.14.*"

djangorestframework-stubs="1.8.*"

Сборка проекта тоже довольно простая и без излишеств:


FROM python:3.12-alpine

# Set environment variables

ENV PYTHONUNBUFFERED 1

ENV PYTHONWARNINGS ignore

ENV CURL_CA_BUNDLE ""

ENV POETRY_VIRTUALENVS_CREATE true

ENV PATH "${PATH}:/root/.local/bin"

# Expose port 8000

EXPOSE 8000/tcp

# Set the working directory for the application

WORKDIR /app

# Copy just the dependencies installation from the current directory to the Docker image

COPY pyproject.toml poetry.lock /app/

# Install necessary dependencies

RUN set -ex; \

   apk update; \

   apk add --no-cache --virtual build-deps \

       curl \

       git \

       gcc \

   && rm -rf /var/lib/apt/lists/* \

   && pip install --no-cache-dir --user poetry==2.1.0 \

   && poetry install --no-interaction --no-ansi \

   && pip install  --no-cache-dir --user requests \

   && apk del --no-cache build-deps

# Create link python interpritator

RUN ln -sf $(poetry env info -e) /python

# Copy wait-for script and give it necessary permissions

COPY wait-for /usr/bin/

RUN chmod +x /usr/bin/wait-for

# Copy the current directory contents into the container

COPY . /app/

# Give necessary permissions to entrypoint

RUN chmod +x entrypoint.*

Поскольку целью статьи является сравнение подхода к работе poetry, rue и uv, мы сознательно не будем оптимизировать сборку в Docker-файлах и размер образов. 

Метрики по сборке: скорость и потребление ресурсов

Для замера скорости сборки немного модифицируем Dockerfile и добавим вот такие строки:

# Install necessary dependencies

RUN set -ex; \

   apk update; \

   apk add --no-cache --virtual build-deps \

       curl \

       git \

       gcc \

   && rm -rf /var/lib/apt/lists/* \

   && pip install --no-cache-dir --user poetry==2.1.0

RUN time -v poetry lock # Фиксирование зависимостей

RUN time -v poetry install --no-interaction --no-ansi --all-groups # Установка зависимостей

RUN pip install  --no-cache-dir --user requests \

   && apk del --no-cache build-deps

С помощью команды docker compose -f docker-compose.yml build backend_poetry --no-cache --progress plain запустим сборку. Опция --progress plain нужна, чтобы выводить все логи сборки.

Фиксирование зависимостей заняло 10 секунд.

#9 [backend_poetry  5/12] RUN time -v poetry lock

#9 0.944 Creating virtualenv uv,_poetry_compare-9TtSrW0h-py3.12 in /root/.cache/pypoetry/virtualenvs

#9 1.449 Resolving dependencies...

#9 10.29    	Command being timed: "poetry lock"

#9 10.29    	User time (seconds): 2.63

#9 10.29    	System time (seconds): 0.95

#9 10.29    	Percent of CPU this job got: 36%

#9 10.29    	Elapsed (wall clock) time (h:mm:ss or m:ss): 0m 9.95s

#9 10.29    	Average shared text size (kbytes): 0

#9 10.29    	Average unshared data size (kbytes): 0

#9 10.29    	Average stack size (kbytes): 0

#9 10.29    	Average total size (kbytes): 0

#9 10.29    	Maximum resident set size (kbytes): 88424

#9 10.29    	Average resident set size (kbytes): 0

#9 10.29    	Major (requiring I/O) page faults: 0

#9 10.29    	Minor (reclaiming a frame) page faults: 138441

#9 10.29    	Voluntary context switches: 388

#9 10.29    	Involuntary context switches: 138

#9 10.29    	Swaps: 0

#9 10.29    	File system inputs: 24

#9 10.29    	File system outputs: 55896

#9 10.29    	Socket messages sent: 0

#9 10.29    	Socket messages received: 0

#9 10.29    	Signals delivered: 0

#9 10.29    	Page size (bytes): 4096

#9 10.29    	Exit status: 0

#9 DONE 10.3s

По трейсу можно понять, что пиковая доля нагрузки CPU заняла бы 36%, при этом памяти было использовано 88 мегабайт. Это, конечно, немного, но скорость в 10 секунд удручает.

А вот установка зависимостей с помощью poetry происходит за пять секунд. По CPU уходило в 100%, так как по факту происходила распаковка скачанных зависимостей. По памяти вышло 88 мегабайт.

#10 [backend_poetry  6/12] RUN time poetry install --no-interaction --no-ansi --all-groups

…

#10 4.846   	Command being timed: "poetry install --no-interaction --no-ansi --all-groups"

#10 4.846   	User time (seconds): 2.48

#10 4.846   	System time (seconds): 2.03

#10 4.846   	Percent of CPU this job got: 100%

#10 4.846   	Elapsed (wall clock) time (h:mm:ss or m:ss): 0m 4.48s

#10 4.846   	Average shared text size (kbytes): 0

#10 4.846   	Average unshared data size (kbytes): 0

#10 4.846   	Average stack size (kbytes): 0

#10 4.846   	Average total size (kbytes): 0

#10 4.846   	Maximum resident set size (kbytes): 88844

#10 4.846   	Average resident set size (kbytes): 0

#10 4.846   	Major (requiring I/O) page faults: 0

#10 4.846   	Minor (reclaiming a frame) page faults: 166516

#10 4.846   	Voluntary context switches: 45109

#10 4.846   	Involuntary context switches: 229

#10 4.846   	Swaps: 0

#10 4.846   	File system inputs: 0

#10 4.846   	File system outputs: 359408

#10 4.846   	Socket messages sent: 0

#10 4.846   	Socket messages received: 0

#10 4.846   	Signals delivered: 0

#10 4.846   	Page size (bytes): 4096

#10 4.846   	Exit status: 0

#10 DONE 5.0s

Переведем проект для использования uv

Для этого нужно создать новый файл pyproject.toml и заново внести в него все зависимости. Я просто добавил все нужные зависимости через CLI с помощью uv add; получился следующий файл:

[project]

name = "uv_poetry_compare"

version = "0.1.0"

requires-python = ">=3.12"

dependencies = [

   "django==4.2",

   "django-debug-toolbar==3.2.1",

   "django-filter==22.*",

   "django-phonenumber-field==5.2.*",

   "django-redis==5.4.*",

   "djangorestframework==3.14.*",

   "drf-spectacular==0.24.*",

   "factory-boy==3.2.0",

   "gunicorn==20.1.0",

   "phonenumbers==8.12.29",

   "pillow==10.0.1",

   "psycopg2-binary==2.9.*",

   "pytest==6.2.4",

   "pytest-cov==2.12.1",

   "pytest-django==4.4.0",

   "pytest-mock==3.6.1",

   "pytest-sugar==0.9.4",

]

[dependency-groups]

lint = [

   "django-stubs==1.14.*",

   "djangorestframework-stubs==1.8.*",

   "mypy==1.*",

   "mypy-extensions==1.*",

   "ruff>=0.9.10",

]

Немного поменяем Docker-файл для замера скорости сборки и установки зависимостей:

FROM python:3.12-alpine

COPY --from=ghcr.io/astral-sh/uv:0.6.5 /uv /uvx /bin/

# Set environment variables

ENV PYTHONUNBUFFERED 1

ENV PYTHONWARNINGS ignore

ENV CURL_CA_BUNDLE ""

ENV POETRY_VIRTUALENVS_CREATE true

ENV PATH "${PATH}:/root/.local/bin"

# Expose port 8000

EXPOSE 8000/tcp

# Set the working directory for the application

WORKDIR /app

# Copy just the dependencies installation from the current directory to the Docker image

COPY . /app/

RUN time -v uv lock

RUN time -v uv sync

# Copy wait-for script and give it necessary permissions

COPY wait-for /usr/bin/

RUN chmod +x /usr/bin/wait-for

# Give necessary permissions to entrypoint

RUN chmod +x entrypoint.*

Для идеального результата воспользуйтесь советами из официальной документации по формированию Dockerfile для продакшен-среды и для локальной разработки.

Метрики по сборке

Сейчас объясню пару нюансов работы uv. 

Формирование uv.lock файла происходит за 4,8 секунды. CPU в пике собрало 50%, а памяти было использовано примерно 73 МБ. По факту скорость — по сравнению с poetry — увеличилась в два раза.

В итоге по формированию uv.lock файла вышли следующие метрики:

#11 [backend_uv stage-0 5/6] RUN time -v uv lock

#11 5.287 Resolved 57 packages in 4.81s

#11 5.300   	Command being timed: "uv lock"

#11 5.300   	User time (seconds): 1.64

#11 5.300   	System time (seconds): 0.91

#11 5.300   	Percent of CPU this job got: 50%

#11 5.300   	Elapsed (wall clock) time (h:mm:ss or m:ss): 0m 5.08s

#11 5.300   	Average shared text size (kbytes): 0

#11 5.300   	Average unshared data size (kbytes): 0

#11 5.300   	Average stack size (kbytes): 0

#11 5.300   	Average total size (kbytes): 0

#11 5.300   	Maximum resident set size (kbytes): 73684

#11 5.300   	Average resident set size (kbytes): 0

#11 5.300   	Major (requiring I/O) page faults: 5

#11 5.300   	Minor (reclaiming a frame) page faults: 173258

#11 5.300   	Voluntary context switches: 3829

#11 5.300   	Involuntary context switches: 175

#11 5.300   	Swaps: 0

#11 5.300   	File system inputs: 592

#11 5.300   	File system outputs: 59472

#11 5.300   	Socket messages sent: 0

#11 5.300   	Socket messages received: 0

#11 5.300   	Signals delivered: 0

#11 5.300   	Page size (bytes): 4096

#11 5.300   	Exit status: 0

#11 DONE 5.3s

А вот с установкой зависимостей все куда интереснее, поскольку если мы cделаем обычный sync, то по трейсу увидим, что все установилось за 0.6 секунд — что, конечно, неправда.

#12 [backend_uv stage-0 6/6] RUN time -v uv sync

#12 0.260 Resolved 57 packages in 0.62ms

#12 0.399 Uninstalled 15 packages in 136ms

#12 0.399  - certifi==2025.1.31

#12 0.399  - charset-normalizer==3.4.1

#12 0.399  - django-stubs==1.14.0

#12 0.399  - django-stubs-ext==5.1.3

#12 0.399  - djangorestframework-stubs==1.8.0

#12 0.399  - idna==3.10

#12 0.399  - mypy==1.15.0

#12 0.399  - mypy-extensions==1.0.0

#12 0.399  - requests==2.32.3

#12 0.399  - ruff==0.9.10

#12 0.399  - tomli==2.2.1

#12 0.399  - types-pytz==2025.1.0.20250204

#12 0.399  - types-pyyaml==6.0.12.20241230

#12 0.399  - types-requests==2.32.0.20250306

#12 0.399  - urllib3==2.3.0

#12 0.402   	Command being timed: "uv sync"

#12 0.402   	User time (seconds): 0.01

#12 0.402   	System time (seconds): 0.13

#12 0.402   	Percent of CPU this job got: 96%

#12 0.402   	Elapsed (wall clock) time (h:mm:ss or m:ss): 0m 0.15s

#12 0.402   	Average shared text size (kbytes): 0

#12 0.402   	Average unshared data size (kbytes): 0

#12 0.402   	Average stack size (kbytes): 0

#12 0.402   	Average total size (kbytes): 0

#12 0.402   	Maximum resident set size (kbytes): 25984

#12 0.402   	Average resident set size (kbytes): 0

#12 0.402   	Major (requiring I/O) page faults: 0

#12 0.402   	Minor (reclaiming a frame) page faults: 1777

#12 0.402   	Voluntary context switches: 62

#12 0.402   	Involuntary context switches: 12

#12 0.402   	Swaps: 0

#12 0.402   	File system inputs: 40

#12 0.402   	File system outputs: 0

#12 0.402   	Socket messages sent: 0

#12 0.402   	Socket messages received: 0

#12 0.402   	Signals delivered: 0

#12 0.402   	Page size (bytes): 4096

#12 0.402   	Exit status: 0

#12 DONE 0.4s

Поясню ситуацию, почему цифры врут. Uv так устроен, что очень много использует кэши разного уровня (более подробно о них можно почитать тут). Для более релевантного замера нужно добавить параметры -n --reinstall к uv sync, чтобы понять реальное время установки пакетов.

/app # time -v uv sync -n --reinstall

Resolved 58 packages in 0.74ms

  	Built pytest-sugar==0.9.4

Prepared 40 packages in 2.82s

Uninstalled 40 packages in 427ms

░░░░░░░░░░░░░░░░░░░░ [0/40] Installing wheels...                                                                                                                                                                                                                                                           	warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance.

     	If the cache and target directories are on different filesystems, hardlinking may not be supported.

     	If this is intentional, set export UV_LINK_MODE=copy or use --link-mode=copy to suppress this warning.

Installed 40 packages in 313ms

    	Command being timed: "uv sync -n --reinstall"

    	User time (seconds): 1.13

    	System time (seconds): 2.86

    	Percent of CPU this job got: 99%

    	Elapsed (wall clock) time (h:mm:ss or m:ss): 0m 4.00s

    	Average shared text size (kbytes): 0

    	Average unshared data size (kbytes): 0

    	Average stack size (kbytes): 0

    	Average total size (kbytes): 0

    	Maximum resident set size (kbytes): 59764

    	Average resident set size (kbytes): 0

    	Major (requiring I/O) page faults: 0

    	Minor (reclaiming a frame) page faults: 127023

    	Voluntary context switches: 31795

    	Involuntary context switches: 870

    	Swaps: 0

    	File system inputs: 0

    	File system outputs: 446232

    	Socket messages sent: 0

    	Socket messages received: 0

    	Signals delivered: 0

    	Page size (bytes): 4096

    	Exit status: 0

Здесь цифры уже ближе к реальности. Установка заняла четыре секунды и 100% CPU (60МБ оперативной памяти). Это чуть быстрее и оптимальнее, чем poetry, но в установке сложно выиграть время, ведь тут просто скачиваются и распаковываются архивы. Однако если делать тесты «на горячую», то есть используя стандартные кэши, то выигрыш будет весомее (как в первом примере с uv sync).

Отличие lock файла в poetry и в uv

Сразу скажу — серьезных отличий между uv.lock и poetry.lock нет, поскольку они придерживаются структуры, которая описана в PEP 751. Это позволяет быстро добавить поддержку для анализаторов кода и проверок на безопасность, например, в trivy. 

0f4cb2c5544b280a76ed70bc8412f6e3.png

И напоследок rye…

…но его мы тестировать не будем. Почему? А потому что uv встроен в rye, и сборка проекта происходит на базе uv, так что смысла в этом нет. Как было сказано выше, rye, в первую очередь, инструмент для локальной разработки, который позволяет обойтись без docker.  

Но мы посмотрим насколько удобно переносить сборку проекта под rye. Для этого надо адаптировать pyproject.toml. 

[tool.rye]

universal = true

virtual = true

Затем запустим команду rye sync. Это вызовет uv и сгенерирует два файла requirements.lock и requirements-dev.lock, после чего будут установлены зависимости в virtualenv в папке .venv.

Dockerfile теперь выглядит так:

 FROM python:3.12-alpine

COPY --from=ghcr.io/astral-sh/uv:0.6.5 /uv /uvx /bin/

# Set environment variables

ENV PYTHONUNBUFFERED 1

ENV PYTHONWARNINGS ignore

ENV CURL_CA_BUNDLE ""

ENV POETRY_VIRTUALENVS_CREATE true

ENV PATH "${PATH}:/root/.local/bin"

# Expose port 8000

EXPOSE 8000/tcp

# Set the working directory for the application

WORKDIR /app

# Copy just the dependencies installation from the current directory to the Docker image

COPY . /app/

RUN time -v uv pip install --no-cache --system -r requirements.lock

# Copy wait-for script and give it necessary permissions

COPY wait-for /usr/bin/

RUN chmod +x /usr/bin/wait-for

# Give necessary permissions to entrypoint

RUN chmod +x entrypoint.*

Установка зависимостей в моем случае прошла за 7.4 секунды. В целом, ничего особенного. 

#11 [backend_rye stage-0 5/8] RUN time -v uv pip install --no-cache --system -r requirements.lock

…

#11 7.276   	Command being timed: "uv pip install --no-cache --system -r requirements.lock"

#11 7.276   	User time (seconds): 2.03

#11 7.276   	System time (seconds): 2.68

#11 7.276   	Percent of CPU this job got: 66%

#11 7.276   	Elapsed (wall clock) time (h:mm:ss or m:ss): 0m 7.04s

#11 7.276   	Average shared text size (kbytes): 0

#11 7.276   	Average unshared data size (kbytes): 0

#11 7.276   	Average stack size (kbytes): 0

#11 7.276   	Average total size (kbytes): 0

#11 7.276   	Maximum resident set size (kbytes): 74864

#11 7.276   	Average resident set size (kbytes): 0

#11 7.276   	Major (requiring I/O) page faults: 185

#11 7.276   	Minor (reclaiming a frame) page faults: 247591

#11 7.276   	Voluntary context switches: 33758

#11 7.276   	Involuntary context switches: 313

#11 7.276   	Swaps: 0

#11 7.276   	File system inputs: 30312

#11 7.276   	File system outputs: 259792

#11 7.276   	Socket messages sent: 0

#11 7.276   	Socket messages received: 0

#11 7.276   	Signals delivered: 0

#11 7.276   	Page size (bytes): 4096

#11 7.276   	Exit status: 0

#11 DONE 7.4s

Заключение

Сравнение инструментов Poetry, uv и rye показало активное развитие экосистемы управления зависимостями в Python. Poetry все еще сохраняет позицию зрелого и многофункционального инструмента, закрывающего большинство потребностей разработчиков. В то же время uv предлагает существенное ускорение операций с зависимостями, подтвержденное тестами скорости фиксации и установки. Rye, интегрируя uv, стремится к созданию единого стандарта разработки, но его практическое применение требует чуть иного подхода, нежели в poetry и uv.

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

Весь код можно глянуть в этом репозитории.

Ну, а на этом всё, если что хотите добавить или рассказать свое видение — заходите в комментарии :)

© Habrahabr.ru