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 --python 3.13
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
Use the Python version your production environment runs in both commands; this example uses 3.13. The initialization command sets requires-python; pin the development interpreter explicitly:
$ uv python pin 3.13
Pinned `.python-version` to `3.13`
The pin selects the development interpreter. uv resolves the lockfile across the Python range declared by requires-python; importing .in files can select different versions from the old compiled files even when the interpreter matches production.
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'. The installer selects entries for its environment. See how to export requirements.txt from a uv project for group selection and project exclusion.
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. Export requirements.txt from the lockfile when the deployment tool requires it. Regenerate the file after changing dependencies.
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.
This handbook is free, independent, and ad-free. If it saved you time, consider sponsoring it on GitHub.