# How to migrate a Django project from pip-tools to uv

Django projects that use [pip-tools](https://pydevtools.com/handbook/reference/pip-tools.md) 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](https://pydevtools.com/handbook/how-to/migrate-requirements.txt.md) does not address, and importing it verbatim puts Django itself in the dev group.

This guide maps the layers onto [uv](https://pydevtools.com/handbook/reference/uv.md) dependency groups so the structure survives the move.

## Prerequisites

- A Django project with a `requirements/` directory compiled by `pip-compile`
- [uv installed](https://pydevtools.com/handbook/how-to/how-to-install-uv.md) on your machine

## Steps

### 1. Read the include chain

Open each `.in` file and note which layer it pulls in:

```console
$ head -1 requirements/*.in
==> requirements/base.in <==
django>=5.0,<5.2

==> requirements/dev.in <==
-r base.txt

==> requirements/test.in <==
-r base.txt
```
```powershell
PS> Get-ChildItem requirements\*.in | ForEach-Object { "==> $($_.Name) <=="; Get-Content $_ -TotalCount 1 }
==> base.in <==
django>=5.0,<5.2
==> dev.in <==
-r base.txt
==> 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 {{< term "transitive-dependency" "transitive dependency" >}} 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

```console
$ 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:

```console
$ 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

```console
$ 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:

```toml {filename="pyproject.toml"}
[project]
dependencies = [
    "django>=5.0,<5.2",
    "django-environ>=0.14.0",
    "psycopg[binary]>=3.3.4",
]
```

Exact versions go to the {{< term "lockfile" >}} (`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:
>
> ```toml
> 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:

```console
$ uv add --group dev $(grep -v '^-r ' requirements/dev.in)
$ uv add --group test $(grep -v '^-r ' requirements/test.in)
```
```powershell
PS> $devPkgs = Get-Content requirements\dev.in | Where-Object { $_ -notmatch '^-r ' }
PS> uv add --group dev $devPkgs

PS> $testPkgs = Get-Content requirements\test.in | Where-Object { $_ -notmatch '^-r ' }
PS> uv add --group test $testPkgs
```
Filtering out the `-r` line hands uv only the remaining package names. Verify the result before moving on:

```toml {filename="pyproject.toml"}
[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](https://pydevtools.com/handbook/explanation/what-are-optional-dependencies-and-dependency-groups.md) compose through the `include-group` key, defined by [PEP 735](https://pydevtools.com/handbook/explanation/what-is-pep-735.md), the standard for `[dependency-groups]` in `pyproject.toml`. If `dev.in` pulled in `test.in` under pip-tools, say so:

```toml {filename="pyproject.toml"}
[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:

```console
$ 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:

```console
$ 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`:

```console
$ 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:

```console
$ rm -r requirements/
```
```powershell
PS> Remove-Item -Recurse requirements
```
If a deployment target still reads a flat file, regenerate one from the lockfile instead of hand-maintaining it:

```console
$ 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`:

```console
$ 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](https://pydevtools.com/handbook/tutorial/set-up-a-django-project-with-uv.md) covers the greenfield path.
- **Linting the migrated project?** [Configure Ruff for Django](https://pydevtools.com/handbook/how-to/how-to-configure-ruff-for-django.md) sets up rules that understand Django's idioms.
- **Adding type checking?** [Configure mypy and django-stubs in a uv project](https://pydevtools.com/handbook/how-to/how-to-configure-mypy-and-django-stubs-in-a-uv-project.md) wires the plugin into the new dependency groups.
- **Containerizing the app?** [Use uv in a Dockerfile](https://pydevtools.com/handbook/how-to/how-to-use-uv-in-a-dockerfile.md) installs from `uv.lock` inside images.
- **Running this in CI?** [Set up GitHub Actions with uv](https://pydevtools.com/handbook/tutorial/setting-up-github-actions-with-uv.md) replaces the `pip-compile` cache step.

## Learn More

- [uv dependency groups](https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-groups) documents `include-group` and default-group behavior
- [pip-tools on GitHub](https://github.com/jazzband/pip-tools) hosts the `pip-compile` source and issue tracker
