Быстрее, выше, сильнее: сравнение подходов 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, поскольку по тестам и удобству сборки он показал себя с очень хорошей стороны.

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/894552/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *