Parottasalna

AI, Backend Engineering & Architecture Guides

Learning Notes #75 – Deep Diving in to UV

UV is a fast and efficient Python package manager designed as an alternative to pip, Poetry, and Conda. Built with Rust, UV aims to provide a modern, lightweight, and high-performance package management experience. It is developed by Astral, the creators of Ruff (a fast Python linter and formatter written in Rust).https://docs.astral.sh/uv/

1. Installing UV and Why It Doesn’t Need Python

UV is a fast Python package manager written in Rust and is a binary, designed to be a drop in replacement for pip and virtualenv.

Since UV is a binary, it does not require Python to be installed for its core functionality.

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

This installs uv globally on your system.

1.2 Using Homebrew (macOS & Linux)

brew install astral-sh/uv/uv

1.3 Using Cargo (If You Have Rust Installed)

cargo install --locked uv

1.4 Manual Download

You can download the latest binary from UV GitHub Releases.

2. Managing Python Versions with UV

UV allows you to manage different versions of Python seamlessly. You can install and switch between Python versions easily.

2.1 Listing Available Python Versions

To list all available Python versions,

uv python list

2.2 Installing a Specific Python Version

To install a specific version of Python,

uv python install 3.10.12

2.3 Using a Specific Python Version

To set a specific Python version for your project,

2.3.1 To install a specific version of Python

uv python install 3.10.12

2.3.2 Using a Specific Python Version

To set a specific Python version for your project

uv python pin 3.9

To verify the current Python version in use

uv run python --version

2.4 Removing an Installed Python Version

To remove a Python version no longer needed

uv python uninstall 3.8

3. Running Python Scripts with uv

3.1 Executing Scripts Without Dependencies

For scripts that don’t require external packages, you can run them directly using uv

# example.py
print("Hello, world!")

Execute the script with

uv run example.py

This command ensures the script runs in an appropriate Python environment managed by uv.

3.2 Handling Scripts with Dependencies

When your script depends on external packages, uv allows you to declare these dependencies within the script itself using inline metadata. This approach eliminates the need for separate environment management tools.

To initialize a script with inline metadata, use

uv init --script example.py --python 3.12

This command adds a metadata block to your script,

# /// script
# dependencies = [
#   "requests<3",
#   "rich",
# ]
# ///

import requests
from rich.pretty import pprint

response = requests.get("https://peps.python.org/api/peps.json")
data = response.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])

The # /// script block specifies the required dependencies. When you run the script, uv automatically creates an isolated environment with these dependencies

uv run example.py

3.3 Locking Dependencies for Reproducibility

To ensure consistent behavior across different environments, it’s essential to lock your script’s dependencies. uv supports creating a lockfile for scripts.

uv lock --script example.py

This command generates an example.py.lock file adjacent to your script, capturing the exact versions of the dependencies. Subsequent executions will reference this lockfile, ensuring reproducibility.

3.4 Specifying Python Versions

uv allows you to specify the Python version required for your script directly within the inline metadata

# /// script
# python = "3.10"
# dependencies = [
#   "requests",
# ]
# ///

import requests

print(requests.__version__)

Alternatively, you can specify the Python version at runtime

uv run --python 3.10 example.py

This flexibility ensures your script runs with the intended Python interpreter.

3.5 Using Alternative Package Indexes

If your dependencies are hosted on a custom package index, uv allows you to specify this in the inline metadata.

# /// script
# dependencies = [
#   "custom-package",
# ]
# [tool.uv.index]
# url = "https://example.com/simple"
# ///

Alternatively, you can add the index via the command line

uv add --index "https://example.com/simple" --script example.py 'custom-package'

This ensures uv resolves dependencies using the specified package index.

4. Running Tools with uvx in UV

uvx is a command within UV that runs Python tools in an isolated environment, handling dependencies automatically. This is particularly useful for running CLI-based Python tools without globally installing them or creating separate virtual environments.

Key Advantages of uvx

  • No Virtualenv Setup Required: uvx handles isolation automatically.
  • Fast and Efficient: Uses UV’s dependency resolution, making it faster than pip-based approaches.
  • Reproducible Environments: Ensures tools run with the correct dependencies every time.

4.1 Running a One-Time Command

Instead of manually installing a package globally, you can use uvx to run it in an isolated environment. For example, to run black (a Python code formatter)

uvx black --version

If black isn’t installed, uvx will fetch and execute it automatically.

4.2 Running Tools with Arguments

You can pass arguments just like you would with a normally installed package. For instance, formatting a Python file

uvx black my_script.py

4.3 Running Multiple Commands

You can use uvx to run multiple commands without installing them globally

uvx isort my_script.py
uvx ruff check my_script.py

4.4 Using uvx in a Project

If you have a project requiring multiple tools (like pytest, ruff, and black), instead of installing them globally, you can

uvx pytest tests/
uvx black .
uvx ruff check .

This keeps your development workflow clean and avoids dependency conflicts.

5. Manage Python projects with UV

5.1 Setting Up a New Project

You can create a fresh Python project with uv using the uv init command

uv init my-project
cd my-project

Alternatively, if you already have a directory, you can initialize the project there

mkdir my-project
cd my-project
uv init

Once initialized, uv generates the following files

.
├── .python-version
├── README.md
├── main.py
└── pyproject.toml

The main.py file contains a basic “Hello, World!” script, which you can execute with:

uv run main.py

5.2 Project Structure

A uv-managed project consists of several key components. Along with the files created during initialization, uv also sets up a virtual environment and a lockfile (uv.lock) the first time you run a command such as uv run, uv sync, or uv lock.

Here’s what a complete project structure looks like:

.
├── .venv
│   ├── bin
│   ├── lib
│   └── pyvenv.cfg
├── .python-version
├── README.md
├── main.py
├── pyproject.toml
└── uv.lock

5.2 Understanding Key Files

pyproject.toml

This file stores metadata about your project, including its name, version, description, and dependencies:

[project]
name = "my-project"
version = "0.1.0"
description = "Your project description here"
readme = "README.md"
dependencies = []

You can manually update this file or use uv commands like uv add and uv remove to manage dependencies more easily.

.python-version

This file specifies the Python version that uv should use when creating the virtual environment.

.venv

The .venv directory contains an isolated Python environment where uv installs project dependencies, ensuring separation from system-wide packages.

uv.lock

This lockfile ensures reproducible builds by storing exact dependency versions. Unlike pyproject.toml, which specifies broad requirements, uv.lock contains precise versions installed in the environment. It should be committed to version control.

5.3 Managing Dependencies

You can install dependencies with the uv add command

uv add requests

To specify a version or install from an alternate source

# Install a specific version
uv add 'requests==2.31.0'

# Install directly from a Git repository
uv add git+https://github.com/psf/requests

To remove a package

uv remove requests

To upgrade a specific package while keeping the rest of the environment stable

uv lock --upgrade-package requests

5.4 Running Commands

Use uv run to execute scripts or commands within the project environment. Before running, uv ensures that the lockfile and environment are synchronized.

Example: Running a Flask server on port 3000

uv add flask
uv run -- flask run -p 3000

To run a Python script using project dependencies

# example.py
import flask
print("Hello, World!")

Execute it with

uv run example.py

Alternatively, update the environment manually with uv sync, then activate it before running commands.

6. How uv.lock Works

When you execute uv resolve, UV interacts with package indexes (such as PyPI) to find compatible versions of the requested dependencies.

It then computes a resolution graph, ensuring that all dependencies are satisfied without conflicts. The resolved versions are stored in uv.lock in a structured format, which UV can later use to install the exact same versions efficiently.

The uv.lock file contains

  • Package Name: The name of the dependency.
  • Exact Version: The resolved version pinned for consistency.
  • Hashes: Cryptographic hashes for verifying the integrity of downloaded packages.
  • Dependency Tree: A structured list of direct and transitive dependencies.
  • Metadata: Additional details about the resolution process.

7. How uv.lock Differs from Other Lock Files

Comparison with Pip’s requirements.txt

  • requirements.txt is often manually created and lacks strict version pinning unless explicitly specified.
  • uv.lock ensures precise dependency resolution, making installations fully deterministic.
  • requirements.txt does not store dependency hashes by default, whereas uv.lock includes them for security.

Comparison with Poetry’s poetry.lock

  • poetry.lock is used in Poetry-based projects, but dependency resolution can be slow.
  • UV optimizes dependency resolution, making uv.lock generation significantly faster.
  • uv.lock is designed to work efficiently with both pip-based and UV-based workflows.

Comparison with Conda’s environment.yml

  • environment.yml is YAML-based and includes additional metadata for Conda environments.
  • uv.lock focuses purely on Python dependencies and offers faster resolution.
  • Conda environments often include system-level dependencies, whereas uv.lock strictly manages Python packages.

8. How to Use uv.lock

1. Generating uv.lock

To generate a uv.lock file, run the following command

uv resolve -o uv.lock

This will create or update the uv.lock file with the resolved dependencies, ensuring consistency across installations.

2. Installing Dependencies from uv.lock

Once the lock file is created, you can install dependencies from it using

uv pip sync uv.lock

This ensures all dependencies are installed in a deterministic manner without deviations from the locked versions.

3. Updating Dependencies

To update dependencies and regenerate the uv.lock file, use

uv resolve --update -o uv.lock

This will attempt to fetch newer compatible versions while preserving package stability.

4. Verifying Dependency Integrity

Since uv.lock includes cryptographic hashes, you can verify the integrity of installed dependencies by running

uv verify uv.lock

This ensures that all installed packages match the recorded hashes, preventing tampering or corruption.

9. Why Use uv.lock?

1. Faster Dependency Resolution

UV is optimized for speed, making uv.lock generation and dependency installation significantly faster than Pip and Poetry.

2. Deterministic Installs

Ensuring that every developer and CI/CD pipeline installs the exact same dependencies reduces inconsistencies.

3. Better Compatibility

Unlike requirements.txt, uv.lock captures all transitive dependencies, reducing unexpected version conflicts.

4. Enhanced Security

Including hashes in uv.lock ensures that dependencies cannot be tampered with, adding an extra layer of security.

5. Efficient CI/CD Workflows

By using uv.lock, continuous integration (CI) pipelines can reliably install dependencies without unexpected version shifts, leading to stable builds.

Discover more from Parottasalna

Subscribe now to keep reading and get access to the full archive.

Continue reading