Skip to content

Creating a Python venv from RPM Wheels with uv

Creating Python virtual environments in containerized or offline environments can be challenging. One powerful approach is to leverage RPM package managers to download Python wheels, then use uv to create a lightweight, reproducible virtual environment from those pre-downloaded wheels.

This workflow is particularly useful when:

  • Building containerized applications with strict dependency control
  • Working in air-gapped networks without internet access
  • Creating reproducible builds across multiple machines
  • Optimizing layer caching in Docker builds

Step 1: Generate Your Current Dependencies

Section titled “Step 1: Generate Your Current Dependencies”

Start by capturing your current Python environment:

Terminal window
pip freeze > requirements.txt

This creates a file with pinned versions of all installed packages, like:

Flask==3.0.0
Werkzeug==3.0.1
click==8.1.7
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.3

Extract just the package names and use dnf download to fetch all wheels:

Terminal window
mkdir -p ./wheels
dnf download --destdir ./wheels $(pip freeze | cut -d '=' -f 1)

Breaking this down:

  • pip freeze | cut -d '=' -f 1 - Extracts just the package names from requirements.txt
  • dnf download - Uses the DNF package manager to download wheels
  • --destdir ./wheels - Specifies where to save the wheels

After running this, your ./wheels directory will contain all the .whl files needed for your environment.

Once you have all the wheels, create your virtual environment using uv:

Terminal window
uv venv venv --offline
uv pip install --offline ./wheels/*.whl

Or more concisely, if your versions are already pinned:

Terminal window
uv venv myenv
uv pip install --find-links ./wheels -r requirements.txt

The --find-links option tells uv to search for packages in your wheels directory before trying to fetch from PyPI.

Here’s a practical example combining all steps:

#!/bin/bash
# Create project structure
mkdir -p my-python-project/wheels
cd my-python-project
# Capture current dependencies
pip freeze > requirements.txt
# Download all wheels
dnf download --destdir ./wheels $(pip freeze | cut -d '=' -f 1)
# Create virtual environment with uv
uv venv venv
uv pip install --find-links ./wheels -r requirements.txt
# Activate the venv
source venv/bin/activate
python --version

This approach shines in Docker builds due to layer caching:

FROM fedora:latest
# Install uv and dnf development tools
RUN dnf install -y python3-pip uv
WORKDIR /app
# Copy requirements
COPY requirements.txt .
# Download wheels layer (caches well if requirements.txt doesn't change)
RUN mkdir -p wheels && \
dnf download --destdir ./wheels $(cat requirements.txt | cut -d '=' -f 1)
# Create venv with uv (uses cached wheels)
RUN uv venv venv && \
venv/bin/uv pip install --find-links ./wheels -r requirements.txt
# Copy application code
COPY . .
ENTRYPOINT ["venv/bin/python"]

This Dockerfile separates the slow “download wheels” layer from the application code, so code changes don’t trigger wheel re-downloads.

Reproducibility: Wheels are locked to specific versions, ensuring the same environment everywhere.

Offline Installation: Once wheels are downloaded, you can create environments without internet access.

Layer Caching: In Docker, wheel downloads cache separately from application code.

Fast Iteration: Subsequent environment creation from cached wheels is much faster than downloading from PyPI.

Minimal Disk Usage: uv creates lightweight venvs compared to other tools.

This workflow requires a DNF-based system (Fedora, RHEL, CentOS). For Debian/Ubuntu, use apt download instead:

Terminal window
mkdir -p ./wheels
for pkg in $(pip freeze | cut -d '=' -f 1); do
apt download "$pkg" -o Dir::Cache=./wheels
done

However, note that this downloads .deb files, not wheels. For true wheel-based workflows on Debian, use pip download with a requirements file instead:

Terminal window
pip download -d ./wheels -r requirements.txt

Some packages include compiled extensions. If you encounter architecture mismatches:

  1. Ensure you’re downloading wheels for the correct Python version
  2. Specify the platform explicitly: pip download -d ./wheels --platform manylinux2014_x86_64 -r requirements.txt
  3. Consider building wheels explicitly: pip wheel -w ./wheels -r requirements.txt

If dnf download can’t find a specific package:

  1. Check if it exists in the repository: dnf search package-name
  2. The package might only be available on PyPI, not in the distro repos
  3. Fall back to pip download for those packages

If you prefer a simpler, distribution-agnostic approach, use pip download directly:

Terminal window
mkdir -p ./wheels
pip download -d ./wheels -r requirements.txt
# Then create your venv
uv venv myenv
uv pip install --find-links ./wheels -r requirements.txt

This is more portable but gives you less control over the source of each package.

Using dnf download with uv provides a powerful, reproducible workflow for Python environment management. It’s especially valuable in containerized deployments and offline scenarios, where you want to pre-download dependencies and cache them effectively.

The key insights are:

  • Separate dependency downloading from venv creation for better layer caching
  • Use --find-links to point uv to your pre-downloaded wheels
  • Pin versions in requirements.txt for true reproducibility
  • Combine with Docker for optimal build performance