Skip to main content

uv

What uv actually is

uv is a single binary that does the job of pip, venv, pyenv, pipx, and poetry combined. Mental model: it sits between you and Python, and whenever you run a script it guarantees the right Python version and the right packages are present first, then runs it. You stop thinking about environments at all.

It operates in three modes, and the mode determines where the venv lives:

  1. Inline script (PEP 723) — one .py file with a dependency header. No project, no folder. The venv is ephemeral and lives in the global cache. This is your scraper use case.
  2. Project — a folder with pyproject.toml and uv.lock. The venv is a normal .venv/ directory inside the project. Reproducible across machines via uv sync.
  3. Tool installuv tool install ruff installs a CLI globally, isolated, like pipx.

Where the cached venv is

For inline scripts, the environment lives in uv's global cache, not next to your file. The default cache location is $XDG_CACHE_HOME/uv or ~/.cache/uv on Linux, ~/Library/Caches/uv on macOS, and %LOCALAPPDATA%\uv\cache on Windows. Override it with UV_CACHE_DIR.

Find it on any machine with:

uv cache dir

The folder names are not human-readable. The virtual environment folder names are generated from a hash of the Python version and the package dependency versions, plus the script name. Change a dependency version or rename the script and uv builds a fresh environment. The reason for the global cache is deduplication: a package is downloaded and built only once, and that artifact is reused across all projects and environments on the same machine that need that exact version. So ten scripts using polars share one copy on disk.

Housekeeping when it grows: uv cache clean wipes it, uv cache prune trims stale entries.

For a project, by contrast, the venv is the visible .venv/ in the project root, an ordinary virtualenv you can inspect or delete.

How to install

The standalone installer is the recommended route (no existing Python needed, uv can fetch Python itself):

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

# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

This drops the uv binary in ~/.local/bin (Linux/macOS) or %USERPROFILE%\.local\bin (Windows). Remember that path; it matters for cron below. Alternatives: pipx install uv, pip install uv, brew install uv. Update later with uv self update.

How to run

Inline script, the whole workflow:

# scraper.py
# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx", "polars", "PyMySQL"]
# ///
import httpx, polars as pl, pymysql
# your code
uv run scraper.py

First run resolves, downloads, builds the cached env, then executes. Subsequent runs reuse the cache and start almost instantly. To add a dependency without editing the header by hand: uv add --script scraper.py requests. To make it reproducible: uv lock --script scraper.py produces scraper.py.lock.

You can also make the file directly executable. Add #!/usr/bin/env -S uv run --script as the first line, then chmod +x scraper.py and run it with ./scraper.py.

Cron

Yes, it works, but two things bite people, both about environment, not uv itself.

1. PATH. cron runs with a stripped-down environment. Its default PATH is typically just /usr/bin:/bin, which does not include ~/.local/bin, so a bare uv in a crontab fails with "command not found." Fix: use the absolute path.

# crontab -e
*/15 * * * * /home/youruser/.local/bin/uv run /home/youruser/scrapers/scraper.py >> /home/youruser/logs/scraper.log 2>&1

Find the exact path with which uv. Always redirect stdout and stderr to a log file, otherwise failures vanish silently (cron emails them to a local mailbox you will never read).

2. HOME and the cache. uv needs HOME set so it can locate ~/.cache/uv. User crontabs normally set HOME correctly, so this usually just works. If you run from a system crontab, a container, or a service account where HOME is unset or different, set it explicitly or pin the cache:

HOME=/home/youruser
UV_CACHE_DIR=/home/youruser/.cache/uv
*/15 * * * * /home/youruser/.local/bin/uv run /home/youruser/scrapers/scraper.py >> /home/youruser/logs/scraper.log 2>&1

Two practical cautions for unattended runs. First, uv run will silently re-resolve and pull new package versions if your header is unpinned and a release drops; for a job that must not break at 3am, use a locked project or uv lock --script so versions are frozen. Second, the very first cron execution does the install work, which can take seconds to a minute; warm the cache by running it once manually before trusting the schedule.

Test before scheduling: run the exact cron command line by hand from a minimal shell (env -i HOME=/home/youruser /home/youruser/.local/bin/uv run scraper.py) to catch PATH or HOME problems before cron does.

Sources: uv official docs (docs.astral.sh/uv) for cache, storage, and CLI behavior; uv-cache man page for platform paths; thisdavej.com for the cache-hashing detail. Cron PATH/HOME behavior is standard Unix, not uv-specific.