Python environments

This post is a self-service cheat sheet for Python environment and version management. One of the biggest problems with virtual environments in Python is the multiple tools available and how they play together.

Updated October 2026. I wrote the first version in 2020 using pyenv and venv. I reviewed it because my usage changed: today I use uv for new work and pipenv for the older projects, and I do not mix them in the same project. Some of the old content was also outdated, for example pipenv no longer installs Python by itself. The old pyenv + venv setup is kept at the end.

XKCD - Python Environment

Which one do I use?

  • New script or project: uv
  • Project that already has a Pipfile: pipenv, or move it to uv
  • Run something once, without installing it: uvx or uv run --with
  • pyenv + venv: still works, but it is two tools and more setup than I need now

uv

uv is a single fast binary. It manages the Python versions, the virtual environment, the dependencies and the CLI tools, so it replaces pyenv, venv, pip and pipx for me.

Install a Python version (uv downloads it, no compiling like pyenv) and pin the folder to it. The pin creates the .python-version file

uv python list
uv python install 3.13
uv python pin 3.13

Start a project and add a package. uv creates the .venv and the uv.lock for you

uv init
uv add requests

uv init also creates a pyproject.toml, a .python-version, a README and a src/ folder. The pyproject.toml looks like this

[project]
name = "myproject"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "requests>=2.34.2",
]

Run the code, no need to activate anything

uv run main.py

Recreate the environment on another machine

uv sync

A single script

For one file there is no need for a project. uv keeps the dependencies inside the script itself (PEP 723)

uv add --script report.py requests
uv run report.py

The script now carries its own requirements

# /// script
# requires-python = ">=3.13"
# dependencies = [
#     "requests>=2.34.2",
# ]
# ///
import requests
print(requests.get("https://example.com").status_code)

Try something without installing it

Run with a temporary package, nothing is added to the project

uv run --with rich python -c "import rich; print('ok')"

Run a CLI tool once, or install it for good in its own isolated environment

uvx cowsay -t hi
uv tool install ruff

If you only want a venv and pip, uv does that too, only faster

uv venv --python 3.13
uv pip install requests

pipenv

I keep it for projects that already have a Pipfile. Create the .venv inside the project folder, not in the home directory

export PIPENV_VENV_IN_PROJECT=1

Install a package and run the code. The Pipfile and the Pipfile.lock are created for you

pipenv install requests
pipenv run python main.py

The Python version is in the [requires] section of the Pipfile

[packages]
requests = "*"

[dev-packages]

[requires]
python_version = "3.13"

pipenv does not install Python anymore

In the 2020 version of this post pipenv offered to install a missing Python using pyenv. That only works if pyenv or asdf is installed. Without them (pipenv 2026.8.0) you get this, and you need to point to an interpreter with pipenv --python <path>

Warning: Python 3.13 was not found on your system...
Neither 'pyenv' nor 'asdf' could be found to install Python.
You can specify specific versions of Python with:
$ pipenv --python path/to/python

From pipenv to uv

You can keep both files side by side until you are happy. I tested this with requests and a dev package pytest.

Start from the [requires] section of the Pipfile and create the project

uv init --python 3.13

Add the packages you listed in [packages] and [dev-packages]. Only those, uv resolves the rest and writes the lock file

uv add requests
uv add --dev pytest

A "*" in the Pipfile becomes a minimum version in the pyproject.toml. If the Pipfile pins a version, pass it to uv

uv add "requests==2.31.0"

Check that it works, then remove the old files

uv run pytest
rm Pipfile Pipfile.lock
rm -rf .venv
uv sync

Commit pyproject.toml, uv.lock and .python-version.

Command map

  • pipenv install -> uv sync
  • pipenv install <pkg> -> uv add <pkg>
  • pipenv install --dev <pkg> -> uv add --dev <pkg>
  • pipenv uninstall <pkg> -> uv remove <pkg>
  • pipenv run <cmd> -> uv run <cmd>
  • pipenv lock -> uv lock
  • pipenv --rm -> rm -rf .venv

If you must keep the exact versions from Pipfile.lock, export them and import with -r. Every sub-package ends up in the pyproject.toml with an exact pin, so the file gets noisy. I prefer the clean way above

pipenv requirements > requirements.txt
pipenv requirements --dev-only > dev.txt
uv add -r requirements.txt
uv add --dev -r dev.txt

Keeping the tools updated

Homebrew on Linux keeps the tools up to date (I did not test this one on the server)

brew install python uv pipenv
brew update && brew upgrade

The Python version of each project still comes from uv or the Pipfile.

Other tools

Not for my day to day, but you will meet them

  • venv / virtualenv - standard library baseline (python -m venv .venv)
  • Poetry, PDM, Hatch - project managers, more for publishing libraries to PyPI
  • conda / mamba / pixi - when you need non-Python binaries (CUDA, GDAL, scientific stacks)
  • pipx - CLI tools in isolated environments, same as uv tool install
  • Rye - earlier experiment by the same people, folded into uv
  • direnv - not Python, but activates a .venv when you cd into the folder

pyenv + venv (the old way)

This was my setup in 2020. Pyenv compiles every Python version, so it is slow and needs the build dependencies. Often I saw pyenv, venv and pipenv getting out of sync, and this was the most reliable combination. Python 3.8 is end of life now, use a current version. The .python-version file is the same one uv reads

pyenv install 3.8.7
pyenv local 3.8.7
python3 --version
# Python 3.8.7
python3 -m venv .venv

References