How to Run Python Scripts in Virtual Environments With uv
Run Python scripts in isolated environments with uv: create a venv, run with uv run, add one-off packages, pin a Python version and use it in a shebang.

Python scripts need packages, packages conflict, and the old answers (venv, pip, activate, remembering which environment you are in) are many steps for what should be one. uv, a fast Python package and project manager written in Rust, folds them into a single command. This post covers running scripts, with cron and CI in mind. It follows making a Python script executable. Commands used uv 0.5 on Debian 13.
Run without activating
cd myproject
uv run report.py
uv run python -c "import sys; print(sys.prefix)"/home/ada/myproject/.venv
In a project with a pyproject.toml, uv run creates .venv if missing, installs the dependencies from the lock file and runs the command inside it. If dependencies changed, it syncs first. No source .venv/bin/activate, and nothing leaks into your global environment.
One-off packages
uv run --with requests --with rich fetch.py
uv run --python 3.12 fetch.py--with builds a temporary environment containing those packages in addition to the project, cached for the next run. It is the quick way to try a script that needs a library, without editing any file. --python selects the interpreter version for the run.
Manage the interpreter
uv python install 3.13
uv python pin 3.13
uv python list --only-installeduv python install downloads a standalone build, so a server without the right Python in its package manager is not a blocker. pin writes a .python-version file that tools read. Teams should commit it so that local-versus-server differences in interpreter version go away.
Classic venv commands, faster
uv venv
uv pip install -r requirements.txt
uv pip compile requirements.in -o requirements.txtIf you are not ready to move to projects, uv pip offers the pip interface, resolves and installs quickly, and compile produces a pinned file. For new work, the project workflow (uv init, uv add requests, uv sync) records dependencies in pyproject.toml and uv.lock. Commit both so every machine and CI job resolves the same versions.
A self-running script
#!/usr/bin/env -S uv run --script
import requests
print(requests.get("https://example.org").status_code)The -S flag to env allows several words in the shebang. After chmod +x fetch, running ./fetch starts uv, which reads the script and its dependencies and runs it. Dependencies are declared in the script itself; see single-file scripts with inline dependencies. Cron can call such a script directly, with uv's path on the cron PATH or written absolutely.
Where this does not apply
uv must be installed, which is one more thing on a minimal server; a plain python3 -m venv needs nothing beyond the OS packages. Air-gapped machines need a mirror or a pre-built environment. Packages with system libraries (database drivers, image libraries) still need the OS side installed. For scheduled jobs, the explicit interpreter path from scheduling Python scripts is the stable form.
What the environment is and why running without activating works
A virtual environment is a directory with a copy or link of the interpreter, a site-packages folder and a configuration file. Activation only edits the PATH in your shell so that python resolves to the one in that directory. Nothing else changes, so calling .venv/bin/python script.py directly gives the same behavior as activating, and does it in places where activation is awkward: cron, systemd, CI jobs and Makefiles. The same is true of uv run, which finds the project's environment, creates it if needed, checks it against the lock file and runs the command inside it. That is why people can drop the activation habit entirely with uv, and why a script that runs fine at your prompt but fails in a service usually has an activation assumption buried in it.
Environments are not portable. They contain absolute paths and compiled extensions for one platform and one interpreter version, so copying one to another machine, or to a different directory, breaks it. Recreate from the lock file, not from a copy.
What reproducible means in practice
A script runs reproducibly when the same inputs produce the same environment: the same interpreter version and the same dependency versions, including indirect ones. Writing requests in a requirements file pins nothing, and next month's install may pull a new major version. A lock file records every package with its version and a hash, and installing from it checks the hashes, which protects against a changed or tampered release. With uv, uv lock writes uv.lock and uv sync installs exactly that, and --frozen refuses to update the lock, which is what CI should use so a build never changes dependencies silently. Commit the lock file for applications and scripts, and treat it as source.
The interpreter matters as much as the packages. requires-python in the project metadata states what the code supports, and .python-version records which one to use, which uv reads and can download on demand.
Speed and the trade-offs against pip
uv is written in Rust and resolves and installs packages much faster than pip, mainly through parallel downloads, a global cache that links files from a shared store instead of copying them, and a faster resolver. A cached install into a new environment takes a moment, which makes disposable environments practical, one per script or per test run. The price is a tool outside the Python distribution, so it has to be installed and kept current, and its behavior around edge cases, such as unusual build backends or private indexes with special authentication, differs from pip in places. For a team with strict compliance needs, the most conservative option is still python -m venv and pip with hashes. For nearly everything else, the speed and the lock file are worth the extra tool.
Verify what ran. uv pip list inside the environment, or uv tree, shows the resolved versions, and comparing it to a colleague's output is the quickest way to find a difference behind a bug that appears on one machine only.
Everyday commands side by side
The classic and uv forms map closely. python -m venv .venv becomes uv venv, pip install -r requirements.txt becomes uv pip install -r requirements.txt, and pip freeze has the equivalent uv pip freeze. In a project with a pyproject.toml, uv add requests edits the dependency list and the lock file together, uv remove undoes it, and uv sync makes the environment match the lock. For a command that needs one extra package once, uv run --with rich script.py adds it for that run without touching the project. Keep the habit of checking which interpreter is used with uv run python -c "import sys; print(sys.executable)", since a wrong interpreter explains most reports of a failure on one machine only.
When you move an existing project over, import from the file you have: install from the old requirements into a new environment, check that the tests pass, then let uv write a lock file from the project metadata and commit it.
See the uv scripts guide and the venv module documentation.


