What Happens When You Run `uv run`
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 for you automatically.
In a project, that setup involves finding pyproject.toml, choosing a Python interpreter, creating or reusing .venv, 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.
$ uv run -v pytest 2>&1 | head -3
DEBUG Found project root: `/home/user/myproject`
DEBUG No workspace root found, using project root
DEBUG Discovered project `myproject` at: /home/user/myproject
Two flags alter this behavior:
--project <path>points uv at a specific directory instead of searching from the current one--no-projectskips project discovery entirely, souv runoperates without any project context. Combine it with--withto run a command in a throwaway environment that has only the packages you specify
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.
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:
- The
--pythonflag, if passed on the command line (e.g.,uv run --python 3.12 pytest) - A
.python-versionfile, searched from the project root upward.uv initcreates one of these by default - The
requires-pythonfield inpyproject.toml, which defines the Python versions the project supports
$ uv run -v python --version 2>&1 | grep "Python request"
DEBUG Using Python request `3.14` from version file at `.python-version`
Once uv has a version constraint, it searches for a matching interpreter. It looks in managed installations first (interpreters uv itself has downloaded to ~/.local/share/uv/python/), 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 .venv doesn’t exist, uv creates it:
$ uv run -v python --version 2>&1 | grep -A1 "Checking for Python environment"
DEBUG Checking for Python environment at: `.venv`
Using CPython 3.14.4
Creating virtual environment at: .venv
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:
- Lockfile is missing or stale: uv resolves all dependencies and writes (or updates)
uv.lock, the same operation as runninguv lockmanually - Lockfile is current: uv skips resolution entirely
$ uv run -v pytest 2>&1 | grep "uv.lock"
DEBUG Existing `uv.lock` satisfies workspace requirements
Two flags control this behavior:
| Flag | Behavior |
|---|---|
--frozen |
Uses the existing lockfile without checking freshness. Skips resolution, but sync can still download packages if the environment needs them |
--locked |
Asserts the lockfile is up to date. If uv.lock is missing or stale, uv exits with an error instead of updating it. Ideal for CI, where an outdated lockfile means someone forgot to commit |
During resolution, uv prefers versions already recorded in uv.lock. Existing packages usually stay pinned unless the current constraints exclude them or you explicitly pass --upgrade or --upgrade-package.
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 pip install the new dependency” problem. By default, uv run uses inexact sync: it adds missing packages but does not remove packages that are in .venv but absent from the lockfile.
$ uv run -v python -c "pass" 2>&1 | grep -E "Requirement|Checked"
DEBUG Requirement already installed: requests==2.33.1
DEBUG Requirement already installed: certifi==2026.2.25
Checked 5 packages in 0.56ms
This is different from uv sync, which performs exact sync and removes extraneous packages. The inexact default means that if you manually installed a package into .venv for debugging, uv run won’t remove it.
Pass --no-sync to skip this step entirely. This is useful when you know the environment is already correct and want to avoid even the milliseconds spent checking.
Step 6: Execute the command
With the environment ready, uv spawns a process using the interpreter from step 2 and the .venv from step 3. For tools installed as console scripts (like pytest), uv runs the entry point directly (.venv/bin/pytest). For uv run python script.py, it launches the Python interpreter. Either way, uv itself steps aside once the child process starts, forwarding signals (SIGTERM, SIGINT) so that Ctrl+C works as expected. From this point on, your code runs in ordinary Python with no uv involvement.
Adding packages with --with
The --with flag installs additional packages into a separate cached environment that overlays the project environment. The project’s own .venv is not modified.
uv run --with httpx python -c "import httpx"This is useful for trying a package without adding it to pyproject.toml, or for running a tool that isn’t a project dependency:
uv run --with coverage coverage run -m pytestThe overlay environment is cached based on the set of --with packages, so repeated invocations with the same extras skip installation.
Override individual steps
Each step in the pipeline has a flag or environment variable that modifies or skips it:
| Step | Flag | Effect |
|---|---|---|
| Project discovery | --no-project |
Skips project lookup; runs without project context |
| Project discovery | --project <path> |
Uses a specific directory as the project root |
| Python resolution | --python <version> |
Overrides .python-version and requires-python |
| Locking | --frozen |
Skips lockfile freshness check |
| Locking | --locked |
Errors if lockfile is stale (instead of updating it) |
| Syncing | --no-sync |
Skips environment sync entirely |
| Execution | --with <pkg> |
Adds packages in a cached overlay, not in .venv |
Several of these flags have environment-variable equivalents (UV_PROJECT, UV_NO_PROJECT, UV_PYTHON, UV_FROZEN, UV_LOCKED, UV_NO_SYNC) for use in CI or shell configuration.
Follow a different path with inline script metadata (PEP 723)
The six-step pipeline above applies 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 or reuses 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.pyThe 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
- uv reference page
- uv: A Complete Guide covers broader uv workflows
- When to use uv run vs uvx explains when to reach for
uv runversusuvx - How to use a uv lockfile for reproducible Python environments goes deeper on lockfile workflows
- Running commands in projects (uv documentation) covers
uv runflags and behaviors in full - Project structure and files (uv documentation) explains
pyproject.toml,.venv, anduv.lock - Python version management (uv documentation) details the interpreter discovery and download system