How to migrate a Django project from pip-tools to uv
Django projects that use pip-tools rarely keep a single requirements file. They keep a requirements/ directory where base.in holds the runtime packages and dev.in and test.in each open with an include line such as -r base.txt. That include chain is the one piece a generic requirements.txt migration does not address, and importing it verbatim puts Django itself in the dev group.
This guide maps the layers onto uv dependency groups so the structure survives the move.
Prerequisites
- A Django project with a
requirements/directory compiled bypip-compile - uv installed on your machine
Steps
1. Read the include chain
Open each .in file and note which layer it pulls in:
$ head -1 requirements/*.in
==> requirements/base.in <==
django>=5.0,<5.2
==> requirements/dev.in <==
-r base.txt
==> requirements/test.in <==
-r base.txt
Each layer maps to one destination:
| pip-tools file | uv destination |
|---|---|
requirements/base.in |
[project.dependencies] |
requirements/dev.in |
[dependency-groups] dev |
requirements/test.in |
[dependency-groups] test |
requirements/*.txt |
uv.lock |
Import the .in files, never the .txt files. A compiled base.txt lists every transitive dependencyA package your dependency depends on. When you install requests, its own dependencies (urllib3, certifi, etc.) are transitive dependencies.
at an exact version, so importing it writes packages into pyproject.toml that Django never asked for directly.
2. Create pyproject.toml beside manage.py
$ uv init --bare
Initialized project `mysite`
--bare writes a pyproject.toml with no sample code, no README, and no [build-system] table, which is what a Django site wants. Your manage.py and settings package stay where they are.
3. Pin the interpreter pip-compile used
uv init picks the newest interpreter it can find, which is often not the version your requirements/*.txt files were compiled against. Pin it explicitly:
$ uv python pin 3.13
Pinned `.python-version` to `3.13`
Skipping this step resolves the whole project against a different Python than production runs, which is how psycopg or asgiref end up on versions the compiled files never contained.
4. Import the base layer
$ uv add -r requirements/base.in
Using CPython 3.13.14
Creating virtual environment at: .venv
Resolved 8 packages in 132ms
Installed 6 packages in 182ms
+ asgiref==3.12.1
+ django==5.1.15
+ django-environ==0.14.0
+ psycopg==3.3.4
+ psycopg-binary==3.3.4
+ sqlparse==0.6.0
uv preserves the constraints base.in declares and adds a lower bound for the rest:
[project]
dependencies = [
"django>=5.0,<5.2",
"django-environ>=0.14.0",
"psycopg[binary]>=3.3.4",
]Exact versions go to the lockfileA file that records the exact version of every installed package, so everyone working on the project gets identical installs.
(uv.lock) instead, so pyproject.toml keeps declaring intent while the lockfile handles reproducibility.
5. Import each layer without its include line
Warning
Do not run uv add --group dev -r requirements/dev.in. uv follows the -r base.txt include and flattens the entire base layer into the dev group as exact pins:
dev = [
"asgiref==3.12.1",
"django==5.1.15",
"django-debug-toolbar>=6.3.0",
"django-environ==0.14.0",
"psycopg==3.3.4",
"psycopg-binary==3.3.4",
"ruff>=0.16.5",
"sqlparse==0.6.0",
]Only django-debug-toolbar and ruff belong there. The rest are runtime packages, now pinned twice and pinned harder than base.in ever asked.
Pass only the lines each layer declares for itself:
$ uv add --group dev $(grep -v '^-r ' requirements/dev.in)
$ uv add --group test $(grep -v '^-r ' requirements/test.in)
Filtering out the -r line hands uv only the remaining package names. Verify the result before moving on:
[dependency-groups]
dev = [
"django-debug-toolbar>=6.3.0",
"ruff>=0.16.5",
]
test = [
"pytest>=9.1.1",
"pytest-django>=4.14.0",
]6. Rebuild the layering with include-group
Dependency groups compose through the include-group key, defined by PEP 735, the standard for [dependency-groups] in pyproject.toml. If dev.in pulled in test.in under pip-tools, say so:
[dependency-groups]
dev = [
{include-group = "test"},
"django-debug-toolbar>=6.3.0",
"ruff>=0.16.5",
]No group needs to include the base layer. [project.dependencies] installs alongside every group, which is what the -r base.txt line was doing by hand.
uv sync installs the dev group by default, so one command now covers what pip-sync requirements/dev.txt did:
$ uv sync
Resolved 17 packages in 16ms
7. Run management commands through uv
uv run executes the command inside the project’s virtual environment, ensuring all dependencies are installed first. No activation step, and no stale environment:
$ uv run python manage.py check
System check identified no issues (0 silenced).
Apply the same prefix to migrate, runserver, and createsuperuser. Update the project README and any Makefile targets in the same commit so nobody keeps calling the old venv.
8. Retire pip-compile
Confirm production installs cleanly with groups excluded, which is the new pip-sync requirements/base.txt:
$ uv sync --no-default-groups
Resolved 17 packages in 2ms
Uninstalled 8 packages in 21ms
- django-debug-toolbar==6.3.0
- iniconfig==2.3.0
- packaging==26.3
- pluggy==1.6.0
- pygments==2.21.0
- pytest==9.1.1
- pytest-django==4.14.0
- ruff==0.16.5
Then delete requirements/ and the pip-compile invocations that wrote files into it:
$ rm -r requirements/
If a deployment target still reads a flat file, regenerate one from the lockfile instead of hand-maintaining it:
$ uv export --no-default-groups --format requirements-txt -o requirements.txt
The export carries hashes without extra flags and marks platform-specific packages with environment markers such as psycopg-binary==3.3.4 ; implementation_name != 'pypy'. One file covers every platform, where pip-compile output is tied to the interpreter and platform that produced it.
Add a drift check to CI so the lockfile cannot fall behind pyproject.toml:
$ uv lock --check
Frequently asked questions
Should I import requirements/base.txt or requirements/base.in?
Import the .in file. base.txt is pip-compile output holding every transitive dependency at an exact version, so importing it writes packages into pyproject.toml that the project never asked for directly. The .in file lists what Django actually depends on, and uv.lock captures the resolved transitive set.
Why did importing requirements/dev.in add Django to my dev group?
Django’s layered requirements files open with an include line such as -r base.txt, and uv add -r follows it. uv imports every package from the base layer into the dev group as an exact pin. Strip the include line before importing, then compose the layers with include-group.
How do I install only production dependencies on the server?
Run uv sync --no-default-groups. That installs [project.dependencies] and skips every group, matching the old pip-sync requirements/base.txt behavior. A plain uv sync gives developers the full toolchain because the dev group installs by default.
Can I keep a requirements.txt for my deployment tooling?
Yes. Run uv export --no-default-groups --format requirements-txt -o requirements.txt to regenerate a pinned, hashed file from uv.lock. Treat it as a build artifact that uv rewrites, not a file anyone edits.
Does uv replace pip-compile and pip-sync directly?
uv ships uv pip compile and uv pip sync as faster drop-in replacements for projects that need to keep their requirements files. Migrating to pyproject.toml and uv.lock goes further, replacing the per-layer, per-platform files that pip-compile generates with one universal lockfile.
Pick your next step
- Starting a Django project from scratch? Set up a Django project with uv covers the greenfield path.
- Linting the migrated project? Configure Ruff for Django sets up rules that understand Django’s idioms.
- Adding type checking? Configure mypy and django-stubs in a uv project wires the plugin into the new dependency groups.
- Containerizing the app? Use uv in a Dockerfile installs from
uv.lockinside images. - Running this in CI? Set up GitHub Actions with uv replaces the
pip-compilecache step.
Learn More
- uv dependency groups documents
include-groupand default-group behavior - pip-tools on GitHub hosts the
pip-compilesource and issue tracker
This handbook is free, independent, and ad-free. If it saved you time, consider sponsoring it on GitHub.