uv Python Package Manager


Article written by humans, image generated by bing.

uv is a new package manager that takes over the python ecosystem in a storm. For a nice introduction see the official Getting Started guide.

uv can run your file with environment variables

uv run executes the python file with the virtual environment, you do not need to activate virtual environment anymore. More importantly it also supports environment variables files with the --env-file parameter, which removes the need for the python-dotenv package or additional tools like dotenvx.

uv run --env-file .env main.py

Note that uv run is quite the magic command, it will run a sync and even download python if it is missing.

uv and Docker

In the pipeline we use the following uv commands and settings. See full Dockerfile at the end.

export UV_PYTHON_DOWNLOADS=never
export UV_COMPILE_BYTECODE=1
uv sync --locked --no-dev
uv run --no-sync main.py
  • UV_PYTHON_DOWNLOADS=never: uv would download python if it is missing, we generally do not want that. Should you use uv’s managed Python in production?
  • UV_COMPILE_BYTECODE=1: uv will compile Python source files to bytecode when running uv sync.
  • --locked: uv will throw an error if the lockfile uv.lock is not up-to-date. Very important.
  • --no-dev: uv will not install the dependencies from the development group.
  • --no-sync or UV_NO_SYNC=1 : uv run will not run the sync step again.

uv is a single 27MB executable, which makes it easy to install and download.

Scanning for Security Vulnerabilities

Vulnerability scanners like osv-scanner do not yet support uv.lock github issue, to work around this we manually have to export a requirements file

uv export --output-file requirements.txt --quiet
osv-scanner requirements.txt 

example output if something is found
╭─────────────────────────────────────┬──────┬───────────┬─────────┬─────────┬──────────────────╮
 OSV URL CVSS ECOSYSTEM PACKAGE VERSION SOURCE
├─────────────────────────────────────┼──────┼───────────┼─────────┼─────────┼──────────────────┤
 https://osv.dev/PYSEC-2022-202 7.4 PyPI pyjwt 2.0.0 requirements.txt
 https://osv.dev/GHSA-ffqj-6fqr-9h24
╰─────────────────────────────────────┴──────┴───────────┴─────────┴─────────┴──────────────────╯

Updating Packages

To list outdated packages you can run the command:

uv pip list --outdated  

Similar to the vulnerabilities scanners, the pyproject.toml is not yet supported in many third party updating tools. For this you have to use the exported requirements.txt again.

Recommendation

  • Pipeline should require that packages are pinned in pyproject.toml.
  • PIpeline checks that uv sync --locked does succeed.
  • The requirements.txt is up to date after each change until tooling supports uv.lock.

Docker cheatsheet

.dockerignore must contain .venv

# build container
docker build -t uv-example -f Dockerfile .
# run container and cleanup after
docker run -it --rm uv-example
# shell into container for debugging
docker run -it --rm --entrypoint /bin/bash uv-example

Dockerfile using virtual environment

For security, we run the script under a dedicated user ID and not root (id=0).

FROM python:3.13 AS builder
WORKDIR /app

RUN pip install uv

ENV UV_PYTHON_DOWNLOADS=never \
    UV_COMPILE_BYTECODE=1 \
    UV_NO_SYNC=1

# Copy the requirements file into the container
COPY uv.lock pyproject.toml /app/

# Install the dependencies
RUN /usr/local/bin/uv sync --locked --no-dev

FROM python:3.13 AS base
# Create a dedicated user to run the app
RUN groupadd --gid=1654  app \
    && useradd -l --uid=1654 --gid=1654 --shell /bin/false app \
    && install -d -m 0755 -o 1654 -g 1654 "/app"

# UID of the non-root user 'app'
ENV APP_UID=1654

WORKDIR /app

COPY --chown=$APP_UID:$APP_UID  . /app/

COPY --from=builder --chown=$APP_UID:$APP_UID /app /app

ENV VIRTUAL_ENV=/app/.venv
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

USER $APP_UID
CMD ["python", "main.py"]

Dockerfile using uv run

FROM python:3.13
RUN groupadd --gid=1654  app \
    && useradd -l --uid=1654 --gid=1654 --home-dir /app --shell /bin/false app \
    && install -d -m 0755 -o 1654 -g 1654 "/app"

# UID of the non-root user 'app'
ENV APP_UID=1654

WORKDIR /app

RUN pip install uv

ENV UV_PYTHON_DOWNLOADS=never \
    UV_COMPILE_BYTECODE=1 \
    UV_NO_SYNC=1

# the lock file is copied first to avoid re-installing dependencies
COPY uv.lock pyproject.toml /app/
RUN uv sync --locked --no-dev

COPY . .

USER $APP_UID
CMD ["uv", "run", "main.py"]