Migrating Pipenv projects to uv


⚠️ NOTICE

This uses Pipenv version 2024.4.1 and uv version 0.6.2 (6d3614eec 2025-02-19).

Details may vary between versions of both, but the basic workflow should be the same.

For a long time, I've been a happy user of Pipenv. It worked "well enough" for what I want a Python project manager to do, principally pinning a Python version, pinning packages, and providing a common entry point to scripts through pipenv run.

The state of the art has rolled on, and a new "good enough" package manager has arrived.

It's called uv. It aims to replace pip, poetry, venv, pipx, and even pipenv with a single binary. It makes heavier use of caching, enabling faster project builds (especially important in CI pipelines). I decided it was time to migrate some personal projects to use uv. Here's how I did that with a project which used pipenv.

Existing Pipenv Projects

Suppose we have a project. This is some old AgriTech tooling that I dug up for this purpose. It doesn't get much love these days.

.
├── agri_tools
│   ├── crop_index
│   └── __init__.py
├── data
├── notebooks
│   └── README.md
├── Pipfile
├── Pipfile.lock
├── README.md
├── run
└── tests

Like with most Pipenv projects, there's a Pipfile and a Pipfile.lock in there.

$ cat Pipfile
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
jupyter = "*"

[dev-packages]
pytest = "*"
mypy = "*"
pylint = "*"

[requires]
python_version = "3.12"
python_full_version = "3.12.7"

One sore spot of Pipenv is how the pinning is kept exclusively in the Pipfile.lock, which is where you'll find version numbers. It's too big to copy and paste here verbatim, so this is a trimmed one. You will see that it is a big old JSON file.

$ head -30 Pipfile.lock
{
    "_meta": {
        "hash": {
            "sha256": "933295b05d6fcbadd4572bee88c7cf681ecff77ac1d8cae9c2c86cf8bc2933bd"
        },
        "pipfile-spec": 6,
        "requires": {
            "python_full_version": "3.12.7",
            "python_version": "3.12"
        },
        "sources": [
            {
                "name": "pypi",
                "url": "https://pypi.org/simple",
                "verify_ssl": true
            }
        ]
    },
    "default": {
        "anyio": {
            "hashes": [
                "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c",
                "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"
            ],
            "markers": "python_version >= '3.9'",
            "version": "==4.6.2.post1"
        },
        "argon2-cffi": {
            "hashes": [
                "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08",

Migrating a Pipfile and Pipfile.lock to pyproject.toml and uv.lock

The first step is to tackle dependencies. Pipenv does allow you to export dependencies as a requirements.txt, so do that with pipenv requirements > requirements.txt.

If you want development dependencies separate from prod dependencies, you can do so with:

$ pipenv requirements --dev > dev-requirements.txt # for dev
$ pipenv requirements > requirements.txt           # for regular

Creating a uv project

Then run uv init . --bare --python <version>, with the version being the python version that you were previously using.

The --bare creates just the pyproject.toml.

Installing dependencies

Once you have this, import your dependencies with uv add -r requirements.txt.

If you look inside pyproject.toml, you will see that it has added every requirement in requirements. It's considered better practice to keep prod requirements separate from the dev requirements.

If you kept your dev dependencies separate from your prod dependencies, you can import then separately with uv add -r <dev-requirements.txt> --dev.

Runfiles

If you're like me (and you keep consistent entry points via a run shell script in the repository), you may have some pipenv run <script> commands in there.

This is what the diff looked like for me.

diff --git a/run b/run
index d0defb2..092c316 100755
--- a/run
+++ b/run
@@ -17,20 +17,20 @@ EOF
 }

 function run_restore(){
-	pipenv install --dev
+	uv sync
 }

 function run_dev(){
-	pipenv run jupyter lab
+	uv run jupyter lab
 }

 function run_lint(){
-	pipenv run mypy "$THISDIR"/agri_tools/
-	pipenv run pylint "$THISDIR"/agri_tools/
+	uv run python3 -m mypy "$THISDIR"/agri_tools/
+	uv run python3 -m pylint "$THISDIR"/agri_tools/
 }

 function run_unit(){
-	pipenv run pytest
+	uv run pytest
 }

 function run_integration(){

Test the functionality from here. Your unit tests should work as before, and your scripts should too. My jupyterlab worked fine.

Cleanup

Once you're happy, you can remove your pipenv's virtual environment and delete your Pipfile and Pipfile.lock.

You can keep the requirements.txt files if you want and if you have an environment which can't use uv but can use pip install -r, but I assume that any environment I use will have uv on it to keep dev and prod dependency parity.