2  Python projects with uv

TipObjective

Learn how to use uv to create, manage, and maintain Python projects. Understand virtual environments, dependency management, and the modern Python packaging ecosystem.

2.1 Why virtual environments

In Python, virtual environments are not optional. They are essential for any serious project work.

Unlike R’s renv (which primarily helps with reproducibility), Python virtual environments serve a fundamental purpose: isolating project dependencies from the system Python.

Here is why this matters:

  • Different projects need different package versions.
  • System Python library should never be modified directly.
  • Dependency conflicts are common and destructive.
  • Reproducibility requires exact version control.
Warning

Installing packages globally with pip install without a virtual environment will cause conflicts and break system tools. Always use virtual environments. To install Python packages as global command-line tools, use pipx.

2.2 What is uv

uv is a modern Python package and project manager written in Rust. It replaces and improves upon a scattered toolchain:

  • pip (package installation)
  • venv (virtual environment creation)
  • pyenv (Python version management)
  • pip-tools (dependency locking)
  • setuptools (package building)

Benefits of uv:

  • Fast: 10-100x faster than pip due to Rust implementation.
  • Complete: Manages Python versions, dependencies, and builds.
  • Modern: Uses pyproject.toml as the single source of truth.
  • Reliable: Automatic dependency resolution and lock files.

In R terms, uv combines functionality from renv, devtools, usethis, and pak into a single, cohesive tool.

2.3 Python packaging standards

Python has standardized on pyproject.toml as the configuration file for all projects. This is similar to R’s DESCRIPTION file but uses TOML format.

The official Python packaging guide is available at https://packaging.python.org/.

Key concepts:

  • pyproject.toml defines project metadata and dependencies.
  • uv.lock records exact versions (like renv.lock).
  • Build backends (like hatchling) create distributable packages.

2.4 Installing uv

Follow the official installation guide.

macOS and Linux:

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

Windows:

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

Via Homebrew (macOS):

brew install uv

Verify installation:

uv --version

2.5 Updating uv

uv can update itself:

uv self update

Regular updates are important because uv frequently adds support for new Python versions and features.

Note

uv uses Python distributions from the python-build-standalone project. These are optimized, portable Python builds that work consistently across platforms.

2.6 Initialize a project

WarningFor GitHub Codespaces users

If you are using GitHub Codespaces, the pycsr project folder is opened by default. Before creating a new practice project, close this folder via File > Close Folder so your shell returns to the home directory. This prevents uv from getting confused about which uv.lock file to write when you “initialize a new project within an existing project”.

Create a new Python project:

uv init pycsr-example
cd pycsr-example

This creates a basic structure:

pycsr-example/
├── .python-version    # Pinned Python version
├── pyproject.toml     # Project metadata and dependencies
├── README.md          # Project documentation
└── src/
    └── pycsr_example/
        └── __init__.py

Notice the directory name uses hyphens (pycsr-example) while the package name uses underscores (pycsr_example). This is Python convention.

2.6.1 Project structure

The pyproject.toml file contains project configuration:

[project]
name = "pycsr-example"
version = "0.1.0"
description = "Example clinical study report project"
dependencies = []

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

Key sections:

  • [project]: Package metadata.
  • [project.dependencies]: Hard, runtime dependencies.
  • [dependency-groups.dev]: Development dependencies.
  • [build-system]: How to build the package.

2.7 Pin Python version

Specify the exact Python version for your project:

uv python pin 3.14.0

This updates .python-version file so everyone uses the same Python version when they restore the environment.

Important

Use the full MAJOR.MINOR.PATCH version (for example, 3.14.0) rather than just MAJOR.MINOR (for example, 3.14). This prevents drift as new patch versions are released.

Why pin the exact version:

  • Patch releases can introduce subtle behavior changes.
  • Reproducibility requires exact version matching.
  • Regulatory submissions should document the exact Python version.

Check which Python versions are available:

uv python list

Install a specific Python version if needed:

uv python install 3.14.0

2.8 Managing dependencies

2.8.1 Adding dependencies

Add runtime dependencies:

uv add polars plotnine rtflite

Add development-only dependencies:

uv add --dev ruff pytest mypy

This updates pyproject.toml:

[project]
dependencies = [
    "plotnine>=0.15.1",
    "polars>=1.35.2",
    "rtflite>=1.1.0",
]

[dependency-groups]
dev = [
    "mypy>=1.18.2",
    "pytest>=9.0.1",
    "ruff>=0.14.5",
]
Note

By default, uv adds dependencies with >= constraints. This allows updates within compatible versions. The lock file ensures exact versions are used.

2.8.2 Removing dependencies

Remove a package:

uv remove pandas

This removes the package from both pyproject.toml and the environment.

2.9 Lock files and syncing

2.9.1 Creating and updating the lock file

Generate or update the lock file:

uv sync

This creates uv.lock, which records:

  • Exact version of every package.
  • All transitive dependencies.
  • Package hashes for verification.

The lock file ensures reproducibility across different machines and over time.

2.9.2 Upgrading dependencies

To update packages while respecting constraints in pyproject.toml:

uv lock --upgrade

Then synchronize the environment:

uv sync

This is similar to:

  • R: renv::update() followed by renv::snapshot().
  • Node.js: npm update followed by npm install.

The two-step process (lock & sync) gives you control: you can review lock file changes before updating your environment.

2.10 Running commands

You have two options for running commands in your project environment.

2.10.1 Option 1: Activate the virtual environment

source .venv/bin/activate  # macOS/Linux
# or
.venv\Scripts\activate     # Windows

Then run commands directly:

python -m pycsr_example
pytest
ruff check

Deactivate when done:

deactivate

2.10.2 Option 2: Use uv run

Run commands without activation:

uv run python -m pycsr_example
uv run pytest
uv run ruff check
Tip

uv run is convenient for one-off commands and CI/CD scripts. For interactive work, activating the environment is often more ergonomic.

2.10.3 uv run and uvx

uvx runs tools in isolated, temporary environments:

uvx ruff check .
uvx black --check .

Use uvx when:

  • Running tools you don’t want to install in the project.
  • Trying packages without adding them as dependencies.
  • Running scripts that declare their own dependencies.

Use uv run when:

  • Running project code.
  • Running tests.
  • Using project dependencies.

See using tools in uv for details.

2.11 Building and publishing

For creating distributable packages, you need a build backend. The simplest option is hatchling.

Add to pyproject.toml:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

2.11.1 Build wheel

Create distribution files:

uv build

This creates:

  • dist/pycsr_example-0.1.0.tar.gz (source distribution)
  • dist/pycsr_example-0.1.0-py3-none-any.whl (wheel)

2.11.2 Publish to PyPI

Publish to the Python Package Index:

uv publish
Note

Building and publishing are not typically needed for internal clinical reporting projects. However, if you develop reusable tools like table generation packages, open sourcing in a GitHub repository and publishing on PyPI will make them more visible.

2.12 Exercise

Create a small project to practice uv commands:

  1. Initialize a new project called csr-practice.
  2. Pin Python to version 3.14.0 (or latest available).
  3. Add polars as a dependency.
  4. Add pytest as a development dependency.
  5. Examine the generated pyproject.toml and uv.lock files.
  6. Run Python using uv run python --version.
View solution
# Initialize project
uv init csr-practice
cd csr-practice

# Pin Python version
uv python pin 3.14.0

# Add dependencies
uv add polars
uv add --dev pytest

# View configuration
cat pyproject.toml

# Check lock file
cat uv.lock

# Run Python
uv run python --version

Your pyproject.toml should look similar to:

[project]
name = "csr-practice"
version = "0.1.0"
description = "Add your description here"
dependencies = [
    "polars>=1.18.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.3.4",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

2.13 What’s next

Now that you understand uv basics, the next chapter covers the Python package toolchain:

  • Formatting and linting with Ruff.
  • Type checking with mypy.
  • Testing with pytest.
  • Documentation generation.
  • Development workflows for clinical reporting.