Back to all posts
Tutorial
8 min read

How to Migrate requirements.txt to uv

DevToolLab Team

DevToolLab Team

October 24, 2026

How to Migrate requirements.txt to uv

To migrate a pip project to uv, run uv init --bare --python <the version you run today>, then uv add -r requirements.txt, then uv add --dev with your dev requirements, and commit pyproject.toml and uv.lock. The commands take about a minute. Checking what they left out takes longer, and it matters more.

In our test with uv 0.13.5, released October 22, 2026, uv add -r dropped a --extra-index-url line for PyTorch's CPU wheels without printing a warning. The lockfile quietly switched to PyTorch's CUDA build, which turns a 0.21 GB Linux install into 4.08 GB.

The Fastest Way

Paste both files into the requirements.txt to pyproject.toml converter. It writes the dependency list uv add -r would, keeps a PyTorch index as an explicit index with torch pinned to it, leaves the -r requirements.txt line out of the dev group, and drops --hash options. It runs in the browser and does not resolve versions, so run uv lock on the result.

The DevToolLab requirements.txt to pyproject.toml converter with a Django requirements file on the left and the generated uv pyproject.toml on the right, including an explicit pytorch-cpu index and a torch source pin
The DevToolLab requirements.txt to pyproject.toml converter with a Django requirements file on the left and the generated uv pyproject.toml on the right, including an explicit pytorch-cpu index and a torch source pin

Migrate With uv add -r, Step by Step

The project carried through this post is an inventory API for a warehouse team in Denver: Django and Django REST Framework, Postgres through psycopg, and a small PyTorch model that forecasts demand. Its requirements.txt asks for PyTorch's CPU-only wheels, which is what most teams want on a server without a GPU:

text
# Inventory API for the Denver warehouse team
--extra-index-url https://download.pytorch.org/whl/cpu
Django==5.2.7
djangorestframework>=3.16,<4
psycopg[binary]==3.2.10
requests[socks]>=2.32  # carrier rate APIs
gunicorn~=23.0
tomli>=2.0.1; python_version < "3.11"
torch==2.9.0  # demand forecasting model

uv add needs a project to add to, so create a bare pyproject.toml first. Without --python, uv uses its default interpreter, which was Python 3.12 on this machine:

uv init --bare
uv add -r requirements.txt
text
Initialized project `inventory-api`
Resolved 42 packages in 110ms

The dependency list it wrote keeps every version constraint, normalizes names to lowercase and rewrites the marker in uv's own form:

toml
[project]
name = "inventory-api"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "django==5.2.7",
    "djangorestframework>=3.16,<4",
    "gunicorn~=23.0",
    "psycopg[binary]==3.2.10",
    "requests[socks]>=2.32",
    "tomli>=2.0.1 ; python_full_version < '3.11'",
    "torch==2.9.0",
]

The --extra-index-url line is gone, and nothing in the output says so.

Check Where torch Came From

uv.lock records the index each package came from. Two greps show what happened:

Bash
grep -c '^name = "nvidia-' uv.lock
grep -A2 '^name = "torch"' uv.lock
text
15
name = "torch"
version = "2.9.0"
source = { registry = "https://pypi.org/simple" }

torch resolved from PyPI. The uv PyTorch guide explains why that matters: PyPI hosts CPU-only PyTorch wheels for Windows and macOS but GPU-accelerated wheels for Linux. The lockfile now carries 15 nvidia-*-cu12 packages and triton for Linux. Summing the Linux x86_64 wheels for Python 3.12 from both lockfiles gives 4.08 GB with the PyPI build (the torch wheel alone is 899.7 MB) and 0.21 GB with the CPU build (184.4 MB for torch).

The Using uv with PyTorch page in the uv documentation, explaining that many PyTorch wheels are hosted on a dedicated index and that +cpu builds are published on download.pytorch.org/whl/cpu
The Using uv with PyTorch page in the uv documentation, explaining that many PyTorch wheels are hosted on a dedicated index and that +cpu builds are published on download.pytorch.org/whl/cpu

The fix, from the same guide, is to declare the index as explicit and pin torch to it:

toml
[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true

[tool.uv.sources]
torch = { index = "pytorch-cpu" }
text
$ uv lock
Resolved 27 packages in 29ms
Removed nvidia-cublas-cu12 v12.8.4.1
...
Removed nvidia-nvtx-cu12 v12.8.90
Updated torch v2.9.0 -> v2.9.0, v2.9.0+cpu
Removed triton v3.5.0

The lock now holds 2.9.0 for macOS and 2.9.0+cpu for Linux and Windows, both from the PyTorch index, and zero nvidia- packages. explicit = true is what makes this safe: uv only uses that index for packages pinned to it, so requests and everything else keep coming from PyPI.

Add Dev Dependencies Without Copying Production Ones

Many requirements-dev.txt files start with -r requirements.txt so that one pip install sets up everything. uv add --dev -r follows that line too, so our dev group came out with 10 entries: the three dev tools plus all seven production dependencies again. Strip the include before adding:

Bash
uv add --dev -r <(grep -v '^-r' requirements-dev.txt)
toml
[dependency-groups]
dev = [
    "pytest>=8.3",
    "pytest-django>=4.14.0",
    "ruff==0.14.1",
]

uv also gave the unpinned pytest-django a lower bound at the version it resolved; uv add --bounds changes that behavior.

Migrate From requirements.in, Not the Compiled File

If the project uses pip-tools or uv pip compile, its requirements.txt is a generated lockfile: every transitive dependency pinned with ==, plus hashes. We compiled our five top-level web dependencies into a 295-line file with 261 --hash options and ran uv add -r on it. The result was 15 exact pins in [project], including packages nobody chose, such as asgiref and charset-normalizer. djangorestframework>=3.16,<4 had become ==3.18.3, and requests[socks] had become a plain requests pin plus a separate pysocks pin, losing the extra.

Run uv add -r requirements.in instead, the file with the dependencies you actually chose. uv.lock takes over the job the compiled file did, including hashes.

Keep a requirements.txt for Tools That Still Need One

Some deploy targets and scanners still read requirements.txt. uv export writes one from the lockfile:

Bash
uv export --format requirements.txt --no-dev --no-hashes -o requirements.lock.txt

It pins torch==2.9.0+cpu ; sys_platform != 'darwin' but writes no index line, and PyPI does not host that build. Resolving the file against PyPI alone fails with Because there is no version of torch==2.9.0+cpu, so pass --extra-index-url https://download.pytorch.org/whl/cpu to whatever installs it.

Common Errors

error: No `pyproject.toml` found in current directory or any parent directory. uv add only edits an existing project. Run uv init --bare first.

Because only requests[socks]==2.28.1 is available and your project depends on requests[socks]>=2.32. You added the PyTorch index without explicit = true. By default uv takes each package from the first index that has it, and the PyTorch index hosts an old copy of requests, so resolution fails. The error's own hint suggests --index-strategy unsafe-best-match, but the explicit index above is the narrower fix.

Because tensorflow==2.15.0 has no wheels with a matching Python version tag (e.g., `cp312`). uv init picked a newer Python than the project runs on, and an old pin has no wheels for it; this one lists cp39, cp310 and cp311. Recreate the project with uv init --bare --python 3.11, which wrote requires-python = ">=3.11", and the same pin resolved.

error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. Someone edited pyproject.toml without relocking. Run uv lock and commit the result; keep uv sync --locked in CI so this fails loudly instead of installing something unreviewed.

When Not to Migrate Yet

If you only want faster installs, uv pip install -r requirements.txt gives you uv's resolver without changing any files. Hold off on the full move when a deploy target, a security scanner or a teammate's tooling only reads requirements.txt, at least until you have wired uv export into the build. And diff the new uv.lock against the old environment before you deploy: a migration that resolves cleanly can still swap a build, as the torch example shows.

Conclusion

The two commands do most of the work: uv init --bare with the Python version you run today, then uv add -r on your top-level requirements. What they skip is everything that was not a requirement line. Index options vanish, includes in dev files get followed, and a compiled file turns every transitive pin into a direct dependency. Before you commit, grep uv.lock for where each heavy package came from. For our inventory API that one check was the difference between a 0.21 GB and a 4.08 GB install on every Linux server and CI run.

  • requirements.txt to pyproject.toml Converter - produce the whole pyproject.toml from both requirements files, with the PyTorch index set up as explicit, before you run uv lock.
  • PyPI Package Inspector - check a pin's release history and the Python versions it declares before you choose requires-python.
  • TOML to JSON Converter - uv.lock is TOML, so convert it to JSON when you want to filter it with jq instead of grep.
  • Dockerfile Linter - review the Dockerfile after you swap pip install -r requirements.txt for uv sync --locked.

Related Posts

PHP2Shell: How to Check and Patch PHP

PHP2Shell (CVE-2026-109618, CVE-2026-109619) is fixed in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Check the PHP that runs, patch, restart and mitigate.

By DevToolLab Team•

jq select: Filter JSON Arrays by Value

Filter JSON with jq select(): match a field, combine and, or and IN, match substrings, export CSV, and fix the errors jq 1.7.1 and 1.8.2 print.

By DevToolLab Team•

Are Claude Code Mods Safe? We Checked 22

Claude Code mods run unsandboxed with your permissions. claude plugin validate shows what one can do; 9 of 22 public mods reach files, programs or the network.

By DevToolLab Team•