Python dependency tooling in 2026: revisit pip, Poetry and uv before you rebuild another virtualenv

Every Python team has the same conversation at least once a year. One developer has started using uv on a side project and wants to move every service to it, because installs that took a minute now take a few seconds. Another says half the services are already on Poetry, and switching tools again will only create a third way of doing things. A senior engineer asks a quieter question: plain pip now writes a standard lock file, so do we need a separate tool at all?

This post compares the three options most teams actually weigh in 2026: pip (with venv, and pip-tools or the newer pip lock for locking), Poetry and uv. The focus is not which tool is fastest in a demo. It is the daily work around them: how each one resolves and locks dependencies, what the lock file really guarantees, how builds stay reproducible in CI and Docker, how Python versions are managed, how monorepos and private indexes are handled, and who owns and maintains each tool. We look at where each one stands on 27 October 2026, and work through a plan for one invented team that was about to switch everything in one sprint.

Two earlier posts sit next to this one. In dependency updates with Renovate, Dependabot and Updatecli we looked at keeping libraries current. A lock file is what those bots update, so the format you pick here decides how well they work. In vulnerability scanners with Trivy, Grype and OSV-Scanner we compared scanners. A scanner can only report on what it can see, and a clear lock file is the easiest thing for it to read.

A scenario to keep us honest

Take an invented Nashik company that builds traceability, cold storage billing and export paperwork software for grape and onion farmer producer companies in north Maharashtra. About 16 engineers. The product is a Django monolith, three FastAPI services, Celery workers for PDF certificates and SMS, and a shared internal library called nsk-common that holds models, money handling and the certificate templates.

Their Python setup looks like this:

  • The monolith uses requirements.txt files made with pip freeze in 2023, edited by hand since then. Some lines are pinned, some are not.
  • Two FastAPI services moved to Poetry 1.8 last year. One is still on plain pip.
  • nsk-common is published to the GitLab package registry. Services install it with --extra-index-url pointing at that registry, while PyPI stays the main index.
  • Docker images are built from python:3.10-slim. Python 3.10 reached end of life on 1 October 2026.
  • Every developer manages Python versions differently: pyenv, the OS package, or whatever came with the laptop.
  • Nobody checks hashes. CI runs pip install -r requirements.txt with no cache, and the pipeline takes about nine minutes, most of it installing packages.

Two things set off the current discussion. First, a Monday morning build pulled a new minor version of an unpinned transitive dependency of the PDF library, and export certificates for one cooperative came out with broken Marathi text until someone pinned the old version by hand. Second, a security review pointed out that with --extra-index-url, pip treats both indexes as equal and picks the highest version. If anyone uploaded a package called nsk-common with version 99 to PyPI, every build would install it.

After the review, one developer proposed moving everything to uv (“faster, one tool, workspaces”). Another proposed finishing the Poetry migration (“we are already halfway”). The tech lead asked whether pip with a standard lock file would do.

The question this post tries to answer: which tool should this team standardise on, and what has to change apart from the tool?

The short version

  • pip (MIT, a Python Packaging Authority project) is the installer that ships with Python. In 2026 it gained a lot: an experimental pip lock command that writes the standard pylock.toml (since 25.1), experimental installs from pylock.toml (since 26.1), dependency groups, build constraints, and a --uploaded-prior-to cooldown. It still does not manage Python versions or projects. The latest release is 26.2.2.
  • pip-tools (BSD-3-Clause) is the long-standing pip-compile and pip-sync pair. It moved from Jazzband to the PyPA this year after Jazzband announced it was winding down. Latest 7.6.2.
  • Poetry (MIT, community maintained) is a full project manager with its own resolver, poetry.lock, dependency groups and publishing. Since 2.0 (January 2025) it reads standard [project] metadata. It added a minimum release age setting in 2.4. Latest 2.5.1.
  • uv (MIT or Apache-2.0, by Astral) is a Rust tool that covers pip-style installs, project locking with a universal uv.lock, workspaces, Python version downloads, tools and a build backend. It is very fast. It is pre-1.0 (0.13.6), its minor versions can carry breaking changes, and Astral announced on 19 March 2026 that it had agreed to join OpenAI. As of today, OpenAI’s announcement still says the deal is subject to closing conditions.
  • PEP 751 pylock.toml is a final, accepted standard. All three tools can write it in some form, but the main project workflows of Poetry and uv still use their own lock files.
  • For the Nashik team, the plan in this post is: get off Python 3.10 first, fix the index setup before anything else, put every deployable on a real lock file with hashes, use uv for the services and the shared library as one workspace, keep the project metadata standard so that an exit stays cheap, and export pylock.toml as a portable artefact.
  • Neither incident was caused by pip. One was a missing lock file and one was an index setting. Any of the three tools fixes both, if you configure it for that.

Where things stand on 27 October 2026

All times below are converted to IST. Release times are taken from PyPI upload times.

Python itself

  • Python 3.15.0 was released on 9 October 2026 at about 7:24 PM IST. Bug fix releases 3.15.1 (21 October) and 3.15.2 (25 October) have already followed.
  • Python 3.10 reached end of life on 1 October 2026. Python 3.11 is security fixes only, until October 2027. Python 3.14 and 3.15 receive full bug fixes.
  • Python 3.16.0a1 came out on 13 October 2026, with the final release planned for 6 October 2027.

pip

  • Latest release: pip 26.2.2 on 20 October 2026 at about 10:06 PM IST. It only updates the bundled certifi to 2026.10.19, which removes the affected eMudhra/emSign root certificate.
  • pip 26.2 (30 July 2026 IST) added --only-deps, --no-require-hashes, experimental build isolation using real virtual environments, Python 3.15 support, and caching of index responses according to their Cache-Control headers. Because of that caching, newly published packages may not be visible immediately; --refresh-package fixes that. Constraints files no longer affect isolated builds; use --build-constraint. It also fixed CVE-2026-13346, a URL path being decoded twice.
  • pip 26.1 (27 April 2026 IST) dropped Python 3.9, added experimental installs from pylock.toml with -r pylock.toml, and allowed --uploaded-prior-to P3D style durations.
  • pip 26.0 (31 January 2026 IST) added --uploaded-prior-to, PEP 723 inline script metadata with --requirements-from-script, and --only-final and --all-releases.
  • pip 25.1 (26 April 2025) added --group for PEP 735 dependency groups and the experimental pip lock command. In our drill, pip lock in 26.2.2 still printed that it is experimental and may change without warning.
  • Licence and governance: MIT, maintained by volunteers in the PyPA GitHub organisation.

pip-tools

  • Latest release: 7.6.2 on 7 October 2026 at about 10:11 AM IST, for compatibility with click 8.5. Version 7.6.0 (July 2026) added --uploaded-prior-to as a pass-through to pip.
  • Governance: Jazzband announced on 14 March 2026 that it was winding down. The pip-tools leads asked the PyPA to take the project, the vote passed in March, and the repository now lives at github.com/pypa/pip-tools with docs at pip-tools.pypa.io. Licence BSD-3-Clause.
  • In our drill, pip-compile warned that --strip-extras becomes the default in 8.0.0.

Poetry

  • Latest release: Poetry 2.5.1 on 20 September 2026 at about 12:48 PM IST, a one-line fix to 2.5.0 (19 September, about 11:44 PM IST). Version 2.5.0 added Python 3.15 support, stopped sending credentials for an https repository over plain http, fixed cases where credentials of the wrong repository were used on a shared host, and improved resolver and installer performance.
  • Poetry 2.4.0 (3 May 2026) added solver.min-release-age, so new releases must be a given number of days old before Poetry considers them.
  • Poetry 2.3.0 (18 January 2026) added pylock.toml export through poetry-plugin-export and build constraints for dependencies. Poetry 2.2.0 (September 2025) added PEP 735 dependency groups. Poetry 2.1.0 (February 2025) added experimental poetry python commands to manage Python installations.
  • Poetry 2.0.0 (5 January 2025) added support for the standard [project] table (PEP 621), made poetry lock keep existing versions by default, moved poetry export and poetry shell out into plugins, and added poetry sync.
  • Security in 2026: the repository lists a high-rated wheel path traversal that could write files outside the install directory (CVE-2026-34591, published 31 March IST, fixed in 2.3.3) and a low-rated tar extraction issue on old Python 3.10 and 3.11 patch releases (CVE-2026-41140, fixed in 2.3.4). Poetry 2.4.2 (30 August IST) fixed two more path traversal problems and a case where an artefact not listed in the lock file could be installed when the source gave no hash.
  • Licence and governance: MIT, in the python-poetry GitHub organisation, maintained by a community team. There is no company behind it.

uv

  • Latest release: uv 0.13.6 on 27 October 2026 at about 6:15 AM IST, which adds CPython 3.15.2, 3.14.10 and others and reduces memory use on large workspaces.
  • uv 0.13.0 (10 October 2026, about 1:15 AM IST) made Python 3.15 the default version that uv downloads when nothing is pinned, started honouring --require-hashes inside included constraints files, switched to a stricter tar reader, and changed the cache format, so the first run after upgrading may download or rebuild packages.
  • uv 0.13.5 (21 October) rejected certificate chains with the compromised eMudhra intermediate keys and stopped publishing uv Docker images based on Python 3.9 and 3.10.
  • On 8 June 2026 Astral announced uv audit, which checks locked dependencies against OSV, and an opt-in malware check during sync (UV_MALWARE_CHECK=1). Both are preview features.
  • pylock.toml support: uv export --format pylock.toml, uv pip compile -o pylock.toml, and uv pip install -r pylock.toml or uv pip sync pylock.toml. The project workflow keeps uv.lock, because the docs say some uv features cannot be expressed in pylock.toml. In our drill, installing from pylock.toml printed a warning that this is still experimental.
  • Versioning: the uv docs call it stable software used widely in production, but uv bumps the minor version for breaking changes. The uv.lock schema is treated as public API and only changes in a minor release.
  • Security in 2026: the repository lists four advisories, the most recent being a high-rated removal of the emSign root (20 October), a medium Windows path traversal in wheel extraction (CVE-2026-104843, September), a medium arbitrary file write through entry point names (May) and a low arbitrary file deletion through RECORD entries (April).
  • Licence and ownership: dual MIT or Apache-2.0. uv is built by Astral, the company led by Charlie Marsh that also makes Ruff and ty. On 19 March 2026 Astral and OpenAI announced an agreement for Astral to join OpenAI’s Codex team. OpenAI’s page says closing depends on customary conditions including regulatory approval, and that until closing the two remain separate and independent companies. Both say the tools will stay open source. We could not find a primary source saying the deal has closed as of today, so treat uv’s ownership as “Astral today, OpenAI expected”.

Standards that matter here

  • PEP 621 (final): project metadata in the [project] table of pyproject.toml. Now read by all three tools.
  • PEP 735 (final, October 2024): [dependency-groups] for dev, test and docs dependencies that are not published. Supported by pip, Poetry and uv.
  • PEP 751 (final, accepted 31 March 2025): pylock.toml, a standard lock file meant to replace locked requirements.txt files.
  • PEP 723 (final): inline script metadata for single-file scripts.
  • PEP 517 (final): the build backend interface, which is why the tool that installs your dependencies does not have to be the tool that builds your wheel.
  • PEP 708 (dependency confusion protection in the index API) was rejected. Protecting yourself from index confusion is still your configuration’s job.

Nearby options, briefly

  • PDM (MIT, latest 2.29.4 on 27 October 2026) is a standards-first project manager with its own lock file. Smaller community than Poetry or uv.
  • Hatch (MIT, latest 1.18.1) manages environments and builds; its backend hatchling (1.32.4) is a common neutral choice for building wheels.
  • pipx (MIT, 1.18.4) installs command line tools in isolated environments, which uv also does with uv tool.
  • Conda and Pixi suit projects with heavy non-Python native dependencies. They are a different ecosystem and outside this post.

First question: why do you want to change?

Write down the real reason before comparing tools. Each reason has a different, and often cheaper, answer.

  • “Builds broke because a dependency changed.” That is a locking problem. Any lock file with hashes fixes it: pip-compile --generate-hashes, poetry.lock or uv.lock.
  • “An attacker could take over our internal package name.” That is an index configuration problem. Removing --extra-index-url and pinning internal packages to the internal index fixes it in pip (through a proxy), Poetry and uv.
  • “CI is slow.” Often a caching problem first. A warm cache helps pip as well. uv is much faster still, but measure your own pipeline before and after.
  • “Every laptop has a different Python.” That is a version management problem. uv and Poetry can both help; so can a single documented installer and a .python-version file.
  • “We have a shared library and several services.” That is a workspace problem. uv has workspaces built in. Poetry and pip need path dependencies or a private index.
  • “Python 3.10 is end of life.” True, and urgent, and unrelated to which installer you use.

For the Nashik team, the honest list was: no real lock file, a risky index setup, an end-of-life Python, slow CI with no cache, a shared library with awkward release steps, and three different ways of working. Only the last two point at a tool choice.

What each tool is

pip, venv and pip-tools

venv (in the standard library since Python 3.3) creates an isolated environment. pip installs packages into it from an index, by default PyPI, resolving versions with its backtracking resolver (the default since pip 20.3 in November 2020). pip installs what you ask for; it does not track the project, add dependencies to pyproject.toml, or keep the environment in sync with a lock file.

Locking has traditionally come from pip-tools: you write loose requirements in pyproject.toml or requirements.in, pip-compile resolves them into a fully pinned requirements.txt with optional hashes, and pip-sync makes an environment match it exactly. Since 2025, pip lock can write a pylock.toml directly, but it is experimental and, as our drill showed, it locks for the current Python and platform only.

Poetry

Poetry is a project manager. You declare dependencies in pyproject.toml, either in the standard [project] table or in Poetry’s older [tool.poetry] tables. poetry lock resolves them into poetry.lock, which covers all supported Python versions and platforms. poetry sync makes the virtual environment match the lock file. Poetry also builds and publishes packages using its own backend, poetry-core, and supports dependency groups, extras, plugins and multiple package sources with priorities.

Poetry is written in Python and is usually installed with its official installer or pipx, separate from your projects. It creates and manages a virtual environment per project.

uv

uv is a single Rust binary with two faces. The uv pip interface is a fast replacement for pip, pip-tools and virtualenv commands. The project interface (uv init, uv add, uv lock, uv sync, uv run) manages a project from pyproject.toml, writes a universal uv.lock that covers all platforms and Python versions in your requires-python range, and keeps .venv in sync automatically when you run commands.

uv also downloads and manages Python builds (uv python install, uv python pin), runs command line tools (uvx, uv tool install), handles PEP 723 scripts, supports Cargo-style workspaces with one lock file for many packages, publishes to indexes, and has its own build backend, uv_build, which became stable in July 2025.

Side by side

Resolution and lock files

  • pip: the resolver works on the current environment. pip lock writes pylock.toml for one Python and platform; in our drill, pydantic-core had exactly one wheel in it, the one for CPython 3.13 on Linux x86_64. pip-compile also resolves for the current environment, so teams with Mac laptops and Linux servers often keep separate files.
  • Poetry: resolves for all Python versions allowed by the project and all platforms, and writes one poetry.lock with hashes for every file. Since 2.0, poetry lock keeps existing versions unless you ask it to regenerate.
  • uv: universal resolution by default. One uv.lock covers every platform and Python version in range. In our drill, the pylock.toml exported from it carried 122 pydantic-core wheels plus the source distribution. uv pip compile can also produce platform-specific or universal requirements files.

What a lock file guarantees

  • All three record exact versions and file hashes when you ask for them. None of them records what is inside a package beyond its hash.
  • A lock file only helps if CI refuses to proceed when it is stale. uv lock --check, uv sync --locked, poetry check --lock and a pip-compile diff check all do that job.
  • Hashes protect you from a file changing on the index. They do not protect you from a bad version that you locked yourself. That is what reviews, cooldowns and scanners are for.

pylock.toml and portability

  • pip: can write it (pip lock, experimental) and install from it (-r pylock.toml, experimental). In our drill, a venv made with the system Python got the bundled pip 25.1.1, which treated the file as an ordinary requirements file and failed. The pip inside the venv must be 26.1 or newer.
  • Poetry: can export it through poetry-plugin-export; installs use poetry.lock.
  • uv: can export it and install from it through uv pip; projects use uv.lock.
  • In our drill, a pylock.toml exported by uv installed cleanly with pip 26.2.2 and with uv. That is the practical value of the standard today: a neutral artefact that another tool can install, even if each tool keeps its own richer lock file for daily work.

Python version management

  • pip: none. Use the OS packages, python.org installers, the Windows Python install manager, pyenv or container base images.
  • Poetry: finds installed Pythons and can manage installations with the experimental poetry python commands.
  • uv: downloads standalone CPython and PyPy builds, pins with .python-version, and uses them automatically. Since 0.13.0, the default download is Python 3.15 when nothing is pinned, so pin your version.

Workspaces and monorepos

  • pip: no concept of a workspace. Use editable installs or path requirements, or publish the shared library to an index.
  • Poetry: no built-in workspace feature. Path dependencies with develop = true work for small cases; each project still has its own lock file.
  • uv: workspaces with [tool.uv.workspace] members, one shared uv.lock, and --package to run or sync a single member. This is the feature that most often decides the choice for teams with a shared library.

Build backends and metadata

  • All three now work with standard [project] metadata and [dependency-groups].
  • The build backend is a separate choice. pip uses whatever the project declares. Poetry projects usually use poetry-core. uv projects can use uv_build, hatchling, setuptools or any other PEP 517 backend.
  • If you want the option to change frontends later, keep metadata in [project], keep tool-specific settings small, and consider a neutral backend such as hatchling for libraries.

Private indexes and index confusion

  • pip: --extra-index-url merges all indexes and picks the best version across them. In our drill, with a fake internal package at 1.0.0 on one loopback index and an “attacker” copy at 99.0.0 on another, pip installed 99.0.0. The safe pattern is a single index URL pointing at a proxy (Nexus, Artifactory, devpi or similar) that serves internal names only from the internal repository.
  • Poetry: sources have priorities (primary, supplemental, explicit). Packages can be pinned to a source, and the docs recommend this for internal packages to avoid dependency confusion. In our drill, with the internal index as primary and the other as supplemental, Poetry locked 1.0.0.
  • uv: the default first-index strategy stops at the first index that has the package, and indexes can be marked explicit and pinned per package in tool.uv.sources. In our drill uv locked 1.0.0 by default, and 99.0.0 when we switched to unsafe-best-match. The word “unsafe” in that option name is there for a reason.

Supply chain controls

  • Hash checking: pip enforces hashes when any requirement has one, or with --require-hashes. Poetry and uv verify hashes from their lock files. uv 0.13 added a preview flag to require hashes for build dependencies as well.
  • Cooldowns: pip has --uploaded-prior-to (with durations since 26.1), Poetry has solver.min-release-age (2.4), uv has exclude-newer with dates and spans. A few days of delay gives the ecosystem time to catch malicious releases.
  • Auditing: pip-audit works with requirements files and environments. uv has uv audit (preview). Any of them can feed an OSV-based scanner from your lock file.
  • Advisories: in 2026, both Poetry and uv had path traversal or file write bugs in their own installers, and pip fixed a URL decoding bug and a script path issue. Installing a package means unpacking untrusted archives. Keep the installer itself updated like any other dependency.

CI and Docker

  • pip: reliable everywhere and preinstalled. Cache ~/.cache/pip between runs, install with --require-hashes from a compiled file, and build wheels for internal packages once.
  • Poetry: install Poetry itself at a pinned version in CI, then poetry sync --no-root before copying the source for better Docker layer caching. Some teams export a requirements or pylock file and install with pip in the final image.
  • uv: copy the pinned binary from the official image, use a cache mount, and run uv sync --locked --no-install-project before copying source. The uv Docker guide documents this pattern, including UV_COMPILE_BYTECODE and UV_LINK_MODE=copy.

Speed

  • uv’s README describes it as “10-100x faster than pip”. That is Astral’s own benchmark, run by the vendor on its own chosen workloads.
  • In our tiny drill, with warm caches, uv lock took 18 ms, poetry lock 770 ms, pip lock about 1.4 seconds and pip-compile about 2.9 seconds. That is one small run on one machine, not a benchmark. The direction matches what most teams see; the size of the gain on your project is something to measure.

Ownership and support

  • pip: PyPA volunteers, bundled with CPython, no company dependency. Changes are slow and careful by design.
  • pip-tools: now under the PyPA, with a small set of maintainers.
  • Poetry: a community maintainer team, regular releases, no single company behind it.
  • uv: a well funded company team with very frequent releases (six patch releases between 14 and 27 October 2026), pre-1.0 versioning, and a pending acquisition by OpenAI. The code is open source under permissive licences, so a community fork is possible, but nobody can promise who would maintain it.

What actually happens when you install

Following one install from a clean checkout shows where the tools differ and where they do not.

  1. Find Python. pip uses the interpreter it runs in. Poetry uses the active or configured Python. uv uses .python-version or requires-python, and can download a build.
  2. Create the environment. python -m venv, Poetry’s managed venv, or uv’s .venv.
  3. Read the inputs. requirements.txt or pylock.toml for pip, pyproject.toml and poetry.lock for Poetry, pyproject.toml and uv.lock for uv.
  4. Resolve or trust the lock. Without a lock, each tool asks the index for versions and resolves. With a current lock, Poetry and uv skip resolution; pip installs exactly what a compiled file lists.
  5. Fetch and verify. Download wheels or source distributions from the configured index, and check hashes if they are recorded.
  6. Build what is not a wheel. Run the PEP 517 build backend in an isolated environment for any source distribution, including your own project.
  7. Install and record. Unpack into site-packages, write scripts, and record installed files.

Steps 4 and 5 are where both Nashik incidents happened: no lock in step 4, and a merged index list in step 5. A faster tool would not have prevented either; a correctly configured one would.

A worked plan for the Nashik team

Step 1: get off Python 3.10 first

Python 3.10 is past its end of life. The team moves images and CI to Python 3.14, which has a year of bug fixes behind it and is supported until October 2030. Python 3.15 is only a few weeks old; they will test it in a separate CI job and move once their main libraries publish wheels for it. They write 3.14 into a .python-version file in every repository. This step does not depend on the tool choice and should not wait for it.

Step 2: fix the index setup before changing anything else

--extra-index-url goes away everywhere, today, even before any migration. The GitLab package registry stays for publishing nsk-common. In the uv projects, the internal index is declared with explicit = true and nsk-common is pinned to it in tool.uv.sources, so it can only ever come from there. Until every service moves, the remaining pip and Poetry services point at the internal index for nsk-common with a Poetry source pin or, for pip, a single proxy URL.

Step 3: one lock file per deployable, checked in CI

Every service gets a committed lock file with hashes. CI fails if the lock file does not match pyproject.toml (uv lock --check in our drill took 41 ms). Lock updates come only through merge requests from the dependency bot or a developer, never as a side effect of a build. The Monday morning surprise cannot happen when the build is not allowed to resolve.

Step 4: standard metadata and dependency groups

All projects move their metadata to the [project] table, with test and lint tools in [dependency-groups]. Tool-specific sections shrink to index settings and workspace membership. This makes the next step cheaper, and it also makes any later change of tool cheaper.

Step 5: uv for the services and the shared library, as one workspace

The team chooses uv, mainly for three reasons that matched their real list: workspaces for nsk-common plus four services in one repository with one lock file, Python version management that ends the “works on my laptop” problem, and CI time. The two Poetry services move first, since their metadata is already close. The monolith moves last, after its hand-edited requirements are turned into a proper pyproject.toml and the first lock is reviewed line by line.

They keep nsk-common on hatchling as its build backend instead of uv_build, so the library can still be built by pip or Poetry if needed.

Step 6: a Docker pattern everyone copies

A trimmed version of the image they settle on, following the uv Docker guide:

FROM python:3.14-slim
COPY --from=ghcr.io/astral-sh/uv:0.13.6 /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_NO_DEV=1 UV_PYTHON_DOWNLOADS=0
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked --no-install-project
COPY . .
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked
CMD ["/app/.venv/bin/gunicorn", "nsk_api.wsgi"]

In production they pin the uv image by digest, not by tag, and build with the same builder and cache settings they use for every other image. Python comes from the base image, not from a uv download, so the image has one Python.

Step 7: cooldown, audit and update rhythm

They set exclude-newer with a span of a few days in the workspace, with a documented way to override it for an urgent security fix. CI runs a scanner on the lock file, and uv audit is tried as a second opinion while it is in preview. uv itself is pinned in CI and in the Dockerfile, and upgraded on purpose every month after reading the changelog, because minor versions can break things.

Step 8: keep an exit open

Because uv’s future owner is changing, the team writes down a short exit plan. Every CI run exports pylock.toml and stores it as a build artefact, and once a quarter a CI job installs a service from that file with plain pip 26.x to prove it still works. Metadata stays standard. The only uv-specific files are uv.lock and a few [tool.uv] lines. If uv’s direction ever stops suiting them, moving to Poetry, PDM or pip with pylock.toml is a week of work, not a rewrite.

When Poetry or plain pip is the better choice

  • Choose plain pip with pip-tools when you have a few small services, one platform, no shared library, and a strong preference for tools that come from the PyPA. It is the most boring option, and boring is a good property for an installer. Add --require-hashes, a cache and one index URL, and it is enough for many teams.
  • Choose Poetry when your team already knows it, your projects are mainly libraries you publish, you value a community-governed tool with no company ownership question, and you do not need workspaces. Poetry 2.x is a much more standard tool than Poetry 1.x was.
  • Choose uv when you have several Python projects in one repository, need Python version management on every laptop and CI runner, or CI time is a real cost. Accept that you are adopting a pre-1.0 tool with frequent releases and a pending change of ownership, and plan for that.
  • Consider PDM when standards compliance matters most and you want a full project manager without a company behind it.

When not to change at all

  • The problem is configuration. A lock file, hashes and one index URL fix most incidents with the tool you already have.
  • You are in the middle of a Python upgrade. Do one big change at a time.
  • Your project is mostly conda packages. None of these three replaces conda for heavy native dependencies.
  • Nobody will own the new tool. Someone has to pin its version, read its changelog and upgrade it.
  • The reason is a benchmark graph. Measure your own CI first, with a warm cache on the current tool.

A short history, so the choices make sense

pip appeared in 2008 as a better replacement for easy_install. virtualenv came before it, and Python 3.3 added venv to the standard library in 2012. pip-tools followed in 2012, giving teams a way to compile loose requirements into pinned ones. For many years pip had no real resolver; the backtracking resolver became the default in pip 20.3 in November 2020.

The pyproject.toml standards changed the ground under all tools. PEP 517 and 518 separated building from installing, PEP 621 standardised project metadata in 2020, PEP 735 added dependency groups in 2024, and PEP 751 standardised the lock file in March 2025.

Poetry’s first release was in February 2018 and 1.0 came in December 2019. For years it was the main answer to “how do I lock and manage a Python project”, at the cost of its own metadata format. Poetry 2.0 in January 2025 moved to the standard [project] table.

uv’s first release was on 15 February 2024 as a fast pip replacement from Astral, the company behind Ruff. Project management with uv.lock arrived in 0.3.0 in August 2024, the build backend became stable in July 2025, and in March 2026 Astral agreed to join OpenAI.

The histories explain the designs. pip is a careful, standards-first installer that everyone depends on. Poetry grew as a full project manager before the standards existed, and has been moving towards them. uv started from speed and one-tool convenience, and has moved quickly enough to set some of the direction for everyone else.

Operational costs to plan for

  • pip and pip-tools: separate lock files per platform, manual Python management, a proxy for safe private index use, and slower installs without careful caching.
  • Poetry: installing and pinning Poetry itself, plugins for export and shell, separate lock files per project in a monorepo, and resolver time on large dependency trees.
  • uv: monthly upgrades of a pre-1.0 tool whose minor versions can break things, cache format changes after upgrades, preview features that may change, and keeping an eye on governance after the acquisition closes.
  • All three: a lock file review habit, an update bot that understands your lock format, a scanner, a cooldown policy, and one person who owns Python tooling.

A decision guide

  • Still on Python 3.10 or older: upgrade Python first, whatever tool you use.
  • Using --extra-index-url for internal packages: fix that today, before any migration.
  • No lock file: add one with hashes, in the tool you already have.
  • Several projects with a shared library in one repository: uv workspaces are the strongest fit.
  • Mostly published libraries and a team that knows Poetry: stay on Poetry 2.x.
  • Small, single-platform services and a preference for PyPA tools: pip with pip-tools and --require-hashes.
  • Want a portable artefact any tool can install: export pylock.toml from whichever tool you choose.

Checklist before you rebuild another virtualenv

  • Every service runs a supported Python version, pinned in .python-version or the base image.
  • No pipeline uses --extra-index-url for internal packages.
  • Internal package names are pinned to the internal index, so they can never come from PyPI.
  • Every deployable has a committed lock file with hashes.
  • CI fails when the lock file is out of date.
  • Metadata lives in [project], and dev tools live in [dependency-groups].
  • The installer version is pinned in CI and in Dockerfiles.
  • Docker builds install dependencies before copying source, with a cache mount.
  • A cooldown policy is configured and documented, with an override for urgent fixes.
  • A scanner reads the lock file on every merge request.
  • A pylock.toml export is tested with a second installer at least once a quarter.

Common mistakes

  • Using --extra-index-url for private packages. Our drill showed pip picking the higher version from the other index.
  • Switching a uv project to unsafe-best-match to fix one resolution problem. It brings back the same risk.
  • Assuming the pip inside a new venv is current. In our drill, a fresh venv got pip 25.1.1, which could not read pylock.toml at all.
  • Expecting pip lock output to work on other platforms. It locks for the environment you ran it in.
  • Locking the project itself with hashes. pip 26.2.2 refused to verify hashes for the project’s own directory entry in its lock file. Lock dependencies, then install the project separately.
  • Letting uv pick the Python version. Without a pin, uv 0.13 downloads Python 3.15.
  • Upgrading uv without reading the changelog. Minor versions carry breaking changes by policy.
  • Treating speed as the whole decision. Ownership, standards and exit cost matter for a tool you will use for years.

A small hands-on drill

We ran a small local drill on 27 October 2026, between about 10:22 AM and 10:24 AM IST, on a Linux 6.12 x86_64 test machine with the system CPython 3.13.5. We installed pip 26.2.2, uv 0.13.6, Poetry 2.5.1 and pip-tools 7.6.2 from PyPI into a throwaway virtual environment. The only network use was PyPI downloads and two throwaway package indexes served on 127.0.0.1. Nothing was published anywhere.

The test project had two dependencies (httpx and pydantic), a dev dependency group with pytest, and a hatchling build backend.

What we saw:

  • Lock times, one run each, not a benchmark. With empty caches: pip lock about 3.1 seconds, pip-compile --generate-hashes about 3.0 seconds, poetry lock about 1.7 seconds, uv lock about 0.46 seconds. With warm caches: 1.4 seconds, 2.9 seconds, 0.77 seconds and 18 ms. uv sync --locked into a new environment took 0.45 seconds.
  • pip lock. It printed that the command is experimental. Its pylock.toml had 12 packages, including the project itself, and exactly one pydantic-core wheel, for this Python and platform.
  • uv export. The pylock.toml exported from uv.lock had 122 pydantic-core wheels plus the source distribution, covering all platforms.
  • Installing from pylock.toml. A venv created with python3 -m venv got the bundled pip 25.1.1, which treated the file as a requirements file and failed. With pip 26.2.2, installing pip’s own lock file failed because pip could not verify hashes for the project’s directory entry; it worked with --no-require-hashes. The uv-exported file, without the project, installed cleanly with both pip 26.2.2 and uv, with matching versions. Both printed that pylock.toml support is experimental.
  • Index confusion. With a fake internal package at 1.0.0 on one loopback index and a copy at 99.0.0 on another, pip with --extra-index-url installed 99.0.0. uv with the internal index first locked 1.0.0, and 99.0.0 with unsafe-best-match. Poetry with the internal index as primary and the other as supplemental locked 1.0.0.

We did not build Docker images, test private index authentication, let uv or Poetry download Python, tamper with hashes, or run any benchmark. This shows three practical points. Index configuration decides whether dependency confusion is possible, whatever the tool. pylock.toml already works across tools if you export it carefully. And uv’s speed advantage is real in direction, but its size on your project is for you to measure.

Unlearn, re-learn

Unlearn the idea that the installer is the cause of most Python dependency problems. In our scenario, a missing lock file and one index flag did all the damage, and both have fixes in every tool. Unlearn, too, the idea that requirements.txt from pip freeze is a lock file, or that a fast tool is automatically a safe one. Re-learn Python dependencies as a supply chain: a supported Python, standard metadata, one index you trust, a lock file with hashes that CI enforces, a cooldown, a scanner, and an installer you pin and upgrade on purpose. Pick the tool that fits your real list, which for many teams with a monorepo is now uv, and keep your exit cheap with standard metadata and an exported pylock.toml. The tool will change again; the habits are what last.

Sources

comments powered by Disqus

Releted Posts

Kubernetes packaging in 2026: revisit Helm, Kustomize and Timoni before you template another YAML file

Sooner or later every team on Kubernetes has the same argument. One person says Helm charts have become unreadable and the team should move to plain YAML with Kustomize overlays.

Read more

Self-hosted CI in 2026: revisit Jenkins, Woodpecker CI and Tekton before you add another build agent

Most teams that keep builds on their own servers did not choose their CI engine recently. It was chosen years ago, by someone who may have left, on a machine that has been upgraded “when there was time”.

Read more

Dependency updates in 2026: revisit Renovate, Dependabot and Updatecli before another library falls three majors behind

A dependency rarely breaks your system on the day it goes out of date. It breaks it eighteen months later, when a security fix lands only on a major version you skipped, and the upgrade that should have been a ten minute review becomes a two week project.

Read more