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 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:
- 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
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.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