Software Engineering Team CU Dept. of Biomedical Informatics

Introducing uv: A Fast, Portable Python Environment Manager

Introducing uv: A Fast, Portable Python Environment Manager

These blog posts are intended to provide software tips, concepts, and tools geared towards helping you achieve your goals. Views expressed in the content belong to the content creators and not the organization, its affiliates, or employees. If you have any questions or suggestions for blog posts, please don’t hesitate to reach out!

Introduction

Managing Python environments has evolved rapidly over the last decade, but complexity and portability remain challenges.
uv from Astral aims to simplify environment management with exceptional speed, cross-platform portability, and complete feature parity with existing tools.
In this article, we’ll explore the history of Python environment management, consider conda in context with Python ecosystems, and provide an overview of using uv.

What is uv and why does it matter?

figure image

uv is a Python environment management and packaging tool which helps you write and maintain Python software. In context with other similar tools uv is magnitudes faster at completing the same work. This is due largely to Rust bindings which help the Python-focused procedures complete more quickly and a custom dependency resolver (which often consumes large amounts of time). In the following paragraphs we’ll cover some background on this area to help provide context about uv and the domain it assists with.

What are Python packages and environment management?

figure image

Python packages are the primary way you can install and import pre-existing code into your projects (without needing to copy the code into your own project directly). Python environments include all the necessary details (including external package dependencies) to help ensure your projects work through reproducible execution. Python environment management tools are used to help add, remove, or update external package dependencies. They also help you build packages of your own for deployment to others.

Without environment management tools and their related code you will be unable to accurately reproduce your Python environment. This can lead to challenges when it comes to reproducibility (you may see different outcomes or exceptions from system to system). It also can be costly in terms of time (dependency management alone can cause hours of debugging time). Using environment managers with Python is nearly required at this point in time.

A brief history of Python environment management

Python packaging and environment management has evolved since the year 2000. It includes many different styles and ecosystems. Note: This timeline is schematic; events are plotted at their calendar years but the spacing is not to scale.
Python packaging and environment management has evolved since the year 2000. It includes many different styles and ecosystems. Note: This timeline is schematic; events are plotted at their calendar years but the spacing is not to scale.

Python environment management has drastically changed since the year 2000. We provide the below timeline synopsis of some of the bigger changes to this domain for Python. Keep in mind that many of these tools are still supported today but some are deprecated and or in the process of being removed (such as distutils, which was removed from Python 3.12 and future versions).

  1. 2000: distutils & setup.py
    • The original Python standard library tools for packaging and distributing Python projects. distutils allowed developers to define how their projects should be built and installed using a setup.py script.
  2. 2004: setuptools
    • setuptools is an enhanced library building on distutils that introduced additional features like dependency management, easy installation, and entry points, becoming the de facto standard for Python packaging.
  3. 2007: virtualenv (external)
    • virtualenv is a third-party tool to create isolated Python environments, preventing conflicts between project dependencies by isolating them per project.
  4. 2008: pip
    • pip is a package installer for Python that greatly simplified the process of installing and managing Python packages from the Python Package Index (PyPI).
  5. 2010: requirements.txt (widespread adoption)
    • The requirements.txt file is a plain text file format listing project dependencies, which became a standard way to specify and share the exact package versions needed for a Python project.
  6. 2012: conda
    • conda is a cross-platform package and environment manager popular especially in the scientific Python community, able to manage non-Python dependencies as well.

    2012: venv (stdlib)

    • The inclusion of venv in Python’s standard library to create lightweight virtual environments without requiring external tools.
  7. 2017: pyproject.toml (PEP 518)
    • A new configuration file standard called pyproject.toml (PEP 518) aimed at improving and standardizing Python project build metadata, allowing tools to declare build dependencies.

    2017: pipenv

    • pipenv is a tool combining package management and virtual environment management in one, focusing on ease of use and reproducible environments.

    2017: hatch

    • hatch is a modern project manager and build tool focusing on simplicity, speed, and support for multiple Python versions and environments.
  8. 2018: poetry
    • poetry is a comprehensive packaging and dependency management tool that uses pyproject.toml, aiming to simplify dependency resolution and publishing.
  9. 2023: uv
    • uv is a newer tool in the ecosystem (likely referring to a fast build or packaging tool, or a modern environment manager) reflecting ongoing innovation in Python packaging and environment management.

Where are packages hosted?

figure image

Python’s packaging ecosystem mainly revolves mostly around PyPI and Conda. PyPI is the official repository for Python packages and is accessed through pip. It handles pure Python packages well but struggles with packages that require non-Python dependencies or system libraries, which can make installation tricky across different platforms.

Conda is a package and environment manager that supports both Python and non-Python packages, making it popular in data science and scientific computing. It simplifies managing complex dependencies but can be slower and sometimes inconsistent due to multiple package channels. Choosing between PyPI and Conda often depends on whether you need pure Python packages or a more complete environment with system-level libraries.

figure image

Using PyPI and Conda together can be challenging because they manage packages and dependencies differently, which can lead to conflicts and unpredictable behavior. Conda packages often bundle compiled libraries with carefully managed build strings, whereas pip installs only Python wheels or source distributions and isn’t aware of Conda’s binary packages. If you install a library with pip after creating a Conda environment, pip may overwrite a Conda‐provided binary (or install a version Conda doesn’t know about), leading to mismatches. Intermixing dependencies also may involve multiple package manager CLIs (e.g. conda and pip) which can introduce challenges with troubleshooting (each have distinct sub-commands and flags). This complexity often forces developers to carefully manage and isolate environments or choose one system over the other to avoid issues.

figure image

A common approach in Python packaging is to first develop and release a package to PyPI, where it can be easily shared and installed using pip. Once the package is stable and widely used, it may be packaged for Conda—often via the community-maintained conda-forge channel—to support users who rely on Conda environments, especially in scientific computing. This pipeline allows developers to reach the broadest audience while maintaining compatibility with both ecosystems.

How are Python packages distributed?

figure image

In Python packaging, package distributions are the artifacts that users download and install to use a Python project—most commonly as .whl (wheel) or .tar.gz (source distribution) files. A wheel is a pre-built, binary package format (.whl) designed for fast installation without needing to compile code (note: a .whl is really a .zip so you can unzip it to take a look at the contents). Wheels are specific to each operating system type and may include already-compiled extensions from other languages. Wheels are the preferred format for most users and is what tools like pip look for first on PyPI. In contrast, a source distribution (.tar.gz, often called an sdist) contains the raw source code and build instructions; installing from it may require compiling extensions written in other languages, e.g. C, C++, or Rust or resolving more complex dependencies. Source distributions are essential for reproducibility, auditing, and as a fallback when no wheel is available for the user’s platform.

Conda packages, on the other hand, belong to a separate ecosystem built around the Conda package manager. A Conda package is a .tar.bz2 or .conda archive that includes not just Python code, but also compiled binaries and system-level dependencies. This makes Conda particularly useful for scientific computing, where packages often require compiled C/C++/Fortran libraries. Unlike wheels, Conda packages are not tied to Python’s internal packaging standards - they’re built using Conda-specific metadata and managed by Conda environments. While PyPI and pip dominate general-purpose Python packaging, the Conda ecosystem provides a more holistic, environment-based approach—at the cost of being somewhat siloed and less compatible with pure Python tools.

These files are typically uploaded using specific application programming interfaces (APIs) to PyPI, conda-forge, or other similar locations.

uv overview

figure image

Below we’ll provide a quick overview of using uv to accomplish various environment management and packaging tasks.

Installing

curl -LsSf https://astral.sh/uv/install.sh | sh

uv can be installed using a curl reference to a script for your local system. Afterwards, uv becomes a CLI command you may use through your terminal session. See the installation documentation for more information.

Intitializing a project

# create a dir for the project and cd into it
mkdir project_name && cd project_name
# initialize a uv project within the dir
uv init --package

uv provides a nice initializer which can give you boilerplate files to work from. We suggest using the --package structure which helps you create a Pythonic package and uses best practices. Afterwards, the structure will look like the following:

tree ./
# tree output
./
├── pyproject.toml
├── README.md
└── src
    └── project_name
        └── __init__.py

3 directories, 3 files

Let’s break down this file structure so we know what we’re working with:

Adding dependencies with uv

# add a dependency to the project
uv add cowsay

# add a dependency to a "dev" dependency group for the project
uv add pytest --group dev

We can add external dependencies to the project using the uv add command. uv enables you to leverage dependency groups which are a way to distinguish between “production” dependencies and “development” dependencies. For example, we might want to include pytest for software testing during development of the project so we could add it to the dev group (we likely won’t need pytest for the production code). This helps keep the production dependencies light by only including the necessary packages if someone uses non-development work from the project. Using uv add (or similarly, uv sync, which updates the environment) also automatically creates a uv.lock lockfile, which is important for consistent environment mangement.

Lockfiles and reproducibility

figure image

Many modern Python environment managers automatically make use of lockfiles. Lockfiles capture the exact dependency graph and package versions, ensuring that environments can be recreated byte-for-byte. Be sure to check out our in-depth blog post on lockfiles for more information. uv generates a uv.lock file automatically, giving you:

PEP 751 introduces some standardization for Python lockfiles (pylock.toml) but many tools have not yet adopted this standard. uv’s lockfile data cannot yet be fully expressed in pylock.toml files and as a result it still depends on uv.lock files. It’s like that the Python standard for lockfiles will evolve over time and could eventually converge (meaning you’d be able to use multiple tools with the same lockfile).

Processing code through uv environments

# run the boilerplate code through the uv environment
# note: the `python -c` command flag executes inline code.
uv run python -c "import project_name; project_name.main()"

# run pytest through the uv environment
uv run pytest

When we want to process code through the uv environment we can use the uv run command. If you’re used to using conda environments this is akin to conda run -n env_name python. Note: uv does not enable you to activate or “enter into” an implicit shell for the environement like conda activate. Instead, uv uses declarative syntax to ensure the command-line interface (CLI) to the environment is explicit.

When working with other projects you might also need to run an install command in order to have access to the environment. uv skips this step and automatically will install the environment on issuing a uv run (there is no uv install command).

When we use uv run several things happen:

Building Python packages for distribution

# build a package for distribution
uv build

uv provides a build system which enables you to build packages for distribution through the command uv build. By default it creates Pythonic .whl and .tar.gz formats that are common to PyPI. Note that you still have to upload these through other means in order for them to be hosted on common platforms like PyPI. We recommend using the Trusted Publisher method for publishing your packages once readied, which takes advantage of GitHub Actions or similar continuous integration / continuous deployment (CI/CD) tooling.

[build-system]
build-backend = "setuptools.build_meta"
requires = [ "setuptools>=64", "setuptools-scm>=8" ]

Note that you can change your build backends within the pyproject.toml configuration (instead of using uv-build by default). For example, if you wanted to use setuptools you could stipulate something like the above in your pyproject.toml file instead. This can assist with areas where you may like to use dynamic versioning for your work through projects like setuptools-scm.

Migrating existing environments to uv

figure image

Reading this, are you thinking you might want to move your project environment management to uv but sweating the idea that it will be complicated? For users looking to migrate existing environments to uv, tools like migrate-to-uv provide a transition path by converting existing requirements.txt or, for example, Poetry-based pyproject.toml files. This can provide a streamlined and low-cost way to transition projects over to uv.

A uv template for new projects

If you’re interested to use uv and would like to start through a template consider using template-uv-python-research-software. This project is a copier template that lets you quickly get started with uv and other boilerplate files for a Python research software project (including Jupyter notebook support). Please see the documentation of the project for more information on what’s included in the template.

For example, you can use the following commands to use this template:

# install copier
pip install copier

# use the template to create a new copy
# where "new_project" is a new directory which
# will include the template copy files.
copier copy https://github.com/CU-DBMI/template-uv-python-research-software new_project

Conclusion

“The best tool is the one you don’t have to think about using.” - common adage

uv from Astral offers research software engineers a fast, portable, and standards‑compliant environment manager that meets or exceeds the capabilities of existing tools. By leveraging native Python packaging, robust lockfiles, and seamless Jupyter support, uv simplifies reproducible workflows and accelerates development.

Previous post
Understanding Object Storage: A Guide for Research Software Development