Type something to search...
Poetry vs uv vs pip-tools: Choosing a Python Dependency Manager

Poetry vs uv vs pip-tools: Choosing a Python Dependency Manager

Plain pip installs packages, but it doesn't really manage them. It won't remember which packages you asked for versus which came along as dependencies, it won't lock versions so that next month's install matches today's, and pip freeze dumps everything into one flat list. As soon as a project has more than one developer or one deployment, you need something on top.

Three tools dominate that space today: pip-tools, the minimal option that adds locking to pip; Poetry, the all-in-one project manager that popularized lock files in Python; and uv, the newer Rust-based tool that does everything Poetry does, plus Python version management, much faster.

This post compares them on the things that matter in practice: the day-to-day workflow, the lock file, speed, Python versions, publishing, and how each fits into CI. I ran the commands below with pip-tools 7.6, Poetry 2.5, and uv 0.12 on Python 3.13.

What a Dependency Manager Should Do

Before comparing tools, it helps to be clear about the job. A good dependency workflow separates two things:

  1. What you want: your direct dependencies, with loose constraints. "httpx, version 0.28 or newer."
  2. What you got: every package in the tree, direct and indirect, pinned to an exact version, ideally with hashes. "httpx 0.28.1, httpcore 1.0.9, h11 0.16.0, ..."

The first is written by humans and lives in pyproject.toml or a requirements.in file. The second is generated by a tool and lives in a lock file. Installing from the lock file gives you the same environment on every machine. Updating means changing the first and regenerating the second.

All three tools do this. They differ in how much else they do and how they do it.

pip-tools: Locking for pip, Nothing More

pip-tools provides two commands: pip-compile, which resolves a list of direct dependencies into a fully pinned requirements.txt, and pip-sync, which makes a virtual environment match that file exactly.

Workflow

You write your direct dependencies in a requirements.in file:

# requirements.in
httpx>=0.28

Then compile it:

python -m venv .venv
.venv/bin/python -m pip install pip-tools
.venv/bin/pip-compile requirements.in

The generated requirements.txt pins everything and records why each package is there:

anyio==4.15.1
    # via httpx
certifi==2026.7.22
    # via
    #   httpcore
    #   httpx
h11==0.16.0
    # via httpcore
httpcore==1.0.9
    # via httpx
httpx==0.28.1
    # via -r requirements.in
idna==3.20
    # via
    #   anyio
    #   httpx
typing-extensions==4.16.0
    # via anyio

(The real file also starts with a comment header recording the command that generated it.)

Development dependencies go in a second file that is constrained by the first, so shared packages always get the same versions:

# dev-requirements.in
-c requirements.txt
pytest
.venv/bin/pip-compile dev-requirements.in
.venv/bin/pip-sync requirements.txt dev-requirements.txt

pip-sync installs what's missing and uninstalls anything not listed, so the environment never drifts. To upgrade:

.venv/bin/pip-compile --upgrade-package httpx requirements.in   # one package
.venv/bin/pip-compile --upgrade requirements.in                 # everything

pip-compile can also read dependencies straight from pyproject.toml (pip-compile pyproject.toml, with --extra dev for optional dependencies) and add hashes with --generate-hashes, which makes pip verify every downloaded file.

Strengths and Weaknesses

pip-tools is small, stable, and transparent. The output is a normal requirements.txt that any tool understands, so it fits into existing Dockerfiles, deployment scripts, and platforms without changes. There's nothing new to learn beyond pip.

The trade-offs:

  • No environment management. You create and activate the virtual environment yourself.
  • No Python version management. You bring your own interpreter.
  • Platform-specific lock files. pip-compile resolves for the Python version and OS it runs on. If your team develops on macOS and deploys to Linux, platform-specific dependencies can differ, and you may need separate compiled files per platform.
  • Speed. Resolution uses pip internally and can be slow on large dependency trees.
  • No building or publishing. Use build and twine for that.

Poetry: The Integrated Project Manager

Poetry manages the whole project: dependencies, virtual environments, lock file, building, and publishing. Since Poetry 2.0, it uses the standard [project] table in pyproject.toml instead of its own format, which made it much more interoperable.

Work flow

poetry new pdemo
cd pdemo
poetry add httpx
poetry add --group dev pytest

Poetry creates a src/ layout project and updates pyproject.toml as you add packages:

[project]
name = "pdemo"
version = "0.1.0"
description = ""
authors = [
    {name = "Your Name",email = "you@example.com"}
]
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "httpx (>=0.28.1,<0.29.0)"
]

[tool.poetry]
packages = [{include = "pdemo", from = "src"}]

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

[dependency-groups]
dev = [
    "pytest (>=9.1.1,<10.0.0)"
]

Notice the default constraint style: poetry add httpx writes >=0.28.1,<0.29.0, an upper bound based on the current version (Poetry's caret-style default). The lock file, poetry.lock, records exact versions and hashes for every package.

The everyday commands:

TaskCommand
Install from the lock filepoetry install
Make the environment match the lock exactlypoetry sync
Skip dev dependenciespoetry install --without dev
Run a command in the environmentpoetry run pytest
Activate the environmenteval $(poetry env activate)
Upgrade within constraintspoetry update or poetry update httpx
Re-lock after editing pyproject.tomlpoetry lock
Show the treepoetry show --tree
Build and publishpoetry build, poetry publish

Poetry keeps its virtual environments in a central cache directory by default. Most people set poetry config virtualenvs.in-project true so the environment lives at .venv inside the project, where editors find it.

Strengths and Weakness

Poetry is mature and widely used, with good documentation and a plugin system. It has a cross-platform lock file, solid dependency groups, and built-in building and publishing. Many existing projects and teams already use it.

The trade-offs:

  • Speed. Dependency resolution and installation are noticeably slower than uv, especially on large projects or cold caches.
  • Python versions. Poetry recently added experimental poetry python install support, but traditionally it expects you to manage interpreters with something like pyenv.
  • Poetry itself needs a home. It's a Python application with its own dependencies, so it should be installed in isolation (via pipx or the official installer) rather than into your project.
  • Upper-bound defaults. The default caret constraints are reasonable for applications, but for libraries they can cause conflicts for downstream users. You can write any constraint you like by hand.
  • Command changes across versions. Poetry 2.0 removed poetry shell (now a plugin) and poetry export (now the poetry-plugin-export plugin), which tripped up some users upgrading from 1.x.

uv: Everything, Fast

uv is a single Rust binary that covers what pip, pip-tools, virtualenv, Poetry, pyenv, and pipx do. It's the newest of the three, and it has been adopted quickly, largely because of speed: Astral's benchmarks show installs and resolution 10 to 100 times faster than pip, and in practice warm-cache installs often take well under a second.

Workflows

uv init weather-cli
cd weather-cli
uv add httpx
uv add --dev pytest
uv run pytest

uv add writes a lower-bound constraint ("httpx>=0.28.1") to the standard [project] table, puts dev tools in the standard [dependency-groups] table, updates uv.lock, and syncs .venv, all in one step. uv run checks the environment is in sync before every command, so there's no separate "install" step to forget.

TaskCommand
Install from the lock fileuv sync
Fail if the lock is stale (CI)uv sync --locked
Skip dev dependenciesuv sync --no-dev
Run a commanduv run pytest
Upgrade one packageuv lock --upgrade-package httpx
Show the treeuv tree
Install a Python versionuv python install 3.13
Run a tool without installing ituvx ruff check .
Build and publishuv build, uv publish

uv also has a pip-tools-compatible interface. uv pip compile requirements.in -o requirements.txt and uv pip sync requirements.txt are drop-in replacements for pip-compile and pip-sync, which makes it easy to speed up an existing pip-tools project without changing anything else.

The full uv workflow is covered in Managing Python Projects with uv.

Strengths and Weaknesse

uv's advantages are speed, scope, and standards. One binary handles Python installation, environments, locking, tools, single-file scripts with inline dependencies (PEP 723), workspaces for monorepos, and publishing. uv.lock is universal: one file resolves for every platform and Python version your project supports.

The trade-offs:

  • Younger project. uv is still pre-1.0 and moves fast. Commands and defaults occasionally change between releases, so pin the uv version in CI.
  • Single vendor. It's open source (MIT/Apache) but developed primarily by one company, Astral.
  • uv.lock is uv-specific. Other tools can't read it directly, though uv export can write requirements.txt or the standard pylock.toml format.

Side-by-Side Comparison

pip-toolsPoetryuv
Main configrequirements.in or pyproject.tomlpyproject.toml ([project])pyproject.toml ([project])
Lock filerequirements.txtpoetry.lockuv.lock
Lock scopeCurrent platform and PythonCross-platformUniversal (cross-platform)
Creates virtual environmentsNoYesYes
Installs Python versionsNoExperimentalYes
Dev dependency groupsSeparate .in filesYesYes (PEP 735)
Build and publishNo (use build + twine)YesYes
Run CLI tools in isolationNoNoYes (uvx)
Inline script dependenciesNoNoYes (PEP 723)
Workspaces / monoreposNoNo (plugins exist)Yes
Written inPythonPythonRust
Relative speedSlowModerateVery fast
MaturityVery matureMatureYoung but widely adopted

Interoperability: Less Lock-In Than Before

A few years ago, picking a tool meant committing to its file format. That's much less true now:

  • Poetry 2.x and uv both use the standard [project] table (PEP 621) for metadata and dependencies, and both support the standard [dependency-groups] table. Moving between them mostly means swapping lock files and the [build-system] section.
  • PEP 751 defines a standard lock file format, pylock.toml. uv can export it (uv export --format pylock.toml), and recent versions of pip include an experimental pip lock command that writes it. Over time this may let tools share lock files.
  • All three produce standard virtual environments and standard wheels.

If you're unsure, choosing any of them today won't trap you. The pyproject.toml you write will carry over. Its structure is covered in Understanding pyproject.toml.

Which Should You Choose?

Choose uv if

  • You're starting a new project. It's the fastest, does the most, and follows the standards closely.
  • You want one tool to handle Python versions, environments, and dependencies.
  • You have large dependency trees or slow CI installs.
  • You write lots of standalone scripts and want inline dependencies.
  • You work in a monorepo with several related packages.

Choose Poetry if

  • Your team or project already uses it and it works. Migrating has a cost; Poetry 2.x is a solid, standards-compatible tool.
  • You rely on Poetry plugins or its specific workflow.
  • You prefer a project with a long track record and community governance over the newest option.

Choose pip-tools if

  • You want the smallest possible change from plain pip and requirements.txt.
  • Your deployment platform expects requirements.txt and you don't want an export step.
  • You deploy to a single, known platform, so platform-specific lock files aren't a problem.

Even then, consider uv pip compile as a faster drop-in for pip-compile. It reads the same .in files and writes the same requirements.txt.

Migrating Between Them

Moving from pip-tools to uv is the easiest path: start with uv pip compile and uv pip sync, then switch to a pyproject.toml project when you're ready by running uv init --bare and uv add -r requirements.in.

Moving from Poetry 2.x to uv usually takes a few minutes: the [project] table works as-is, you replace the [build-system] with one for your preferred backend (or keep poetry-core, which uv can build with), delete poetry.lock, and run uv lock. For Poetry 1.x projects with dependencies in [tool.poetry.dependencies], the community migrate-to-uv tool automates the conversion.

Whichever direction you go, regenerate the lock file with the new tool and run your test suite before committing. Different resolvers may pick different (still valid) versions.

Conclusion

All three tools solve the core problem of separating what you want from what you install, and all three are reasonable choices. pip-tools adds locking to plain pip with minimal ceremony. Poetry offers an integrated, mature workflow that now speaks standard pyproject.toml. uv covers everything Poetry does plus Python installation, tools, and scripts, at a speed that changes how you work.

For a new project in 2026, I'd start with uv. For an existing Poetry or pip-tools project that's working well, there's no urgency to switch, but uv pip compile is an easy, low-risk way to get some of the speed today.

Tags :
Share :

Related Posts

Abstract Base Classes in Python with the abc Module

Abstract Base Classes in Python with the abc Module

Python leans on duck typing: if an object has the method you need, you call it and move on. That works well until you have a family of classes that a

Continue Reading
*args and **kwargs in Python: Flexible Function Signatures

*args and **kwargs in Python: Flexible Function Signatures

You've seen def wrapper(*args, **kwargs): in decorators, and probably super().__init__(**kwargs) in class hierarchies. These two parameters let a

Continue Reading
Asyncio in Python: A Beginner's Guide to Asynchronous Programming

Asyncio in Python: A Beginner's Guide to Asynchronous Programming

A lot of programs spend most of their time waiting. A web scraper waits for pages to download, an API server waits for the database, a chat bot waits

Continue Reading