Skip to content

What Happens When You Run `uv run`

uv

uv doesn’t replace Python. It orchestrates Python. When you type uv run pytest, uv sets up the right environment and then hands off to a real Python interpreter to do the actual work. The command is equivalent to activating a virtual environment and running python -m pytest inside it, except uv handles every setup step automatically.

That setup involves finding your project, choosing a Python interpreter, creating or reusing a virtual environment, checking the lockfile, and syncing packages. Each step short-circuits if the work is already done, which is why uv run feels instant on repeated invocations but takes longer the first time.

That sequence explains most uv run surprises: unexpected downloads, new virtual environments, and lockfile changes you didn’t ask for.

Two concepts come up repeatedly in the steps below. A virtual environment is an isolated folder (.venv) containing a Python interpreter and installed packages, kept separate from the rest of the system so different projects don’t interfere with each other. A lockfile (uv.lock) records the exact versions of every package so that everyone on the team gets identical installs.

Step 1: Discover the project

uv starts by searching for a pyproject.toml file. It looks in the current working directory first, then walks up through parent directories until it finds one. The first pyproject.toml it encounters becomes the project root.

If uv doesn’t find a pyproject.toml, it runs without project context. In that case it uses an active virtual environment if one exists; otherwise it searches for a Python interpreter and runs the command directly.

You can pass --no-project to skip project discovery entirely, or --project <path> to point uv at a specific directory instead of searching.

Step 2: Resolve a Python interpreter

With the project identified, uv needs a Python interpreter that satisfies the project’s constraints. It checks three sources, in order of priority:

  1. The --python flag, if passed on the command line (e.g., uv run --python 3.12 pytest)
  2. A .python-version file, searched from the project root upward. uv init creates one of these by default
  3. The requires-python field in pyproject.toml, which defines the Python versions the project supports

Once uv has a version constraint, it searches for a matching interpreter. It checks managed installations first (interpreters uv itself has downloaded), then falls back to interpreters on PATH.

If no compatible interpreter exists and automatic downloads are enabled (the default), uv downloads and installs one. This is why uv run works on a machine with no Python installed at all.

Step 3: Create the virtual environment

uv places a virtual environment at .venv next to the project’s pyproject.toml. If .venv already exists and uses a compatible Python version, this step is a no-op.

If the project defines a [build-system] in its pyproject.toml (typical for libraries), uv installs the project itself into the environment as an editable install. This means code changes take effect immediately without re-running uv sync. Application projects created with uv init (no [build-system]) skip this step.

Step 4: Lock dependencies

uv checks whether uv.lock exists and whether it matches the current project metadata. If the lockfile is missing or stale, uv resolves all dependencies and writes or updates uv.lock, the same operation as running uv lock manually. If the lockfile is current, uv skips resolution entirely.

During resolution, uv prefers versions already recorded in uv.lock. Existing packages stay pinned unless the current constraints exclude them or you pass --upgrade.

In CI, you can pass --locked to make uv error if the lockfile is out of date (instead of silently updating it), or --frozen to skip the freshness check altogether.

Step 5: Sync the environment

With the lockfile settled, uv installs any packages that are in uv.lock but missing from .venv. This automatic sync prevents the common “I pulled new code but forgot to install the new dependency” problem.

You can pass --no-sync to skip this step if you know the environment is already correct.

Step 6: Execute the command

With the environment ready, uv runs the command. For tools installed as console scripts (like pytest), uv runs the entry point directly. For uv run python script.py, it launches the Python interpreter. Either way, uv steps aside once the child process starts. From this point on, your code runs in ordinary Python with no uv involvement.

Adding one-off packages with --with

The --with flag installs additional packages into a temporary cached environment without modifying the project’s .venv. This is useful for trying a package without adding it to pyproject.toml:

uv run --with httpx python -c "import httpx"

Standalone scripts bypass the project pipeline

The steps above apply when uv run operates inside a project. When the target is a .py file containing PEP 723 inline script metadata, uv skips the project pipeline entirely. It reads the script’s dependencies and requires-python, creates a cached environment for that script, installs the declared dependencies, and runs the file.

# /// script
# dependencies = ["rich"]
# requires-python = ">=3.12"
# ///

from rich import print
print("[bold green]Hello from a script![/bold green]")
uv run script.py

The script’s environment is isolated from any project. Even if you run the script from inside a uv project directory, the project’s dependencies are not available unless the script declares them.

For more on writing scripts with inline metadata, see How to write a self-contained Python script using PEP 723.

Learn More

Last updated on