Быстрее, выше, сильнее: сравнение подходов 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.

И напоследок 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, поскольку по тестам и удобству сборки он показал себя с очень хорошей стороны.
Весь код можно глянуть в этом репозитории.
Ну, а на этом всё, если что хотите добавить или рассказать свое видение — заходите в комментарии :)