# Do I need a project to use uv?


If your Python life is a folder full of short scripts that glue APIs together and rename files, [uv](https://pydevtools.com/handbook/reference/uv.md)'s first move can look like overkill: `uv init`, then a `pyproject.toml`. uv works fine for scripts, but it assumes a different mental model from the [`pip install`](https://pydevtools.com/handbook/reference/pip.md) habit most script authors carry, and forcing the old habit onto uv is where people get stuck.

The short answer: no, you don't need a project. You have four options, and the right one depends on what the script is.

## Why everyone reaches for one shared environment

Installing packages once and using them everywhere is a rational response to old pip. pip installed globally by default, and a [virtual environment](https://pydevtools.com/handbook/explanation/what-is-a-virtual-environment.md) had to be re-activated in every new shell. A shebang of `#!/usr/bin/env python3` pointed at the one interpreter that had every library installed, and everything worked until it didn't.

That world is gone. Modern Python on macOS (via Homebrew) and most Linux distributions now refuse `pip install` outside a venv, which is what the [externally-managed-environment error](https://pydevtools.com/handbook/how-to/how-to-fix-the-externally-managed-environment-error.md) is trying to tell you. Windows is less strict, but the muscle memory persists everywhere, which is why the first question people ask after installing uv is a version of "where do I put my libraries?"

## How uv makes per-script isolation cheap

Every `uv run` resolves a script's dependencies against a global wheel cache at `~/.cache/uv`. The cache is shared across every script and every project on the machine, so the `requests` wheel used by last week's script can also satisfy today's. Repeat runs of the same script reuse the resolved environment in around 100ms; cold first runs take roughly a second for a handful of packages, longer if uv has to download wheels it has not seen before.

The historical argument for a shared venv was avoiding forty separate installs of `requests`. With uv's cache you install it once and reuse it everywhere, and each script's environment stays independent. See [What happens when you run `uv run`](https://pydevtools.com/handbook/explanation/what-happens-when-you-run-uv-run.md) for the full execution path.

## Let each script declare its own dependencies

For a one-off script that imports a handful of third-party libraries, [PEP 723](https://pydevtools.com/handbook/explanation/what-is-pep-723.md) inline metadata is the right default. The script carries a commented TOML block at the top declaring its Python version and dependencies, and `uv run` reads that block to build the environment.

```python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["requests", "rich"]
# ///

import requests
from rich import print
```

On macOS and Linux, `chmod +x myscript.py` and then `./myscript.py` runs the script the same way it always did. On Windows, which does not act on Unix shebangs, run it with `uv run myscript.py` instead — the inline metadata still drives the environment.

The cost is a four-line header and a shebang edit per script. For existing scripts, that's an afternoon of migration. For new scripts, `uv add --script myscript.py requests` writes the header for you. See [How to write a self-contained Python script](https://pydevtools.com/handbook/how-to/how-to-write-a-self-contained-script.md) for the full walkthrough.

## Share one venv across a batch of scripts

If you have a dozen automation scripts that all use `requests`, `click`, and `rich`, and none of them care which version, a single shared virtual environment is a reasonable choice.

```bash
uv venv ~/.venvs/scripts
uv pip install --python ~/.venvs/scripts/bin/python requests click rich
```

Shebang each script at the absolute interpreter path, then `chmod +x` and run it directly (use `/Users/you/...` on macOS; shebangs can't expand `~` or `$HOME`):

```python
#!/home/you/.venvs/scripts/bin/python
```

```powershell
uv venv $HOME\.venvs\scripts
uv pip install --python $HOME\.venvs\scripts\Scripts\python.exe requests click rich
```

Windows doesn't act on Unix shebangs, so run each script through the venv's interpreter directly:

```powershell
uv run --python $HOME\.venvs\scripts\Scripts\python.exe myscript.py
```

This keeps the mental model you grew up with, minus the fragility: a system package manager upgrading Python (for example, [Homebrew](https://pydevtools.com/handbook/reference/homebrew.md) on macOS or `apt` on Linux) no longer wipes your packages, because the venv holds its own copy of the interpreter.

Eventually one script needs `pydantic==1.x` while another needs `pydantic>=2`, and the shared venv can't hold both. Then you split the venv or migrate the conflicting scripts to PEP 723. Pick this pattern when you own the scripts and update them together; avoid it for scripts you want to leave alone for years.

## Put your scripts in a uv project

The reproducible cousin of the shared venv is a uv project whose only purpose is to host your scripts. One directory contains a `pyproject.toml` listing every dependency and a `uv.lock` pinning exact versions; the scripts sit alongside. `uv run ./myscript.py` from that directory picks up the project environment automatically.

The value over a raw shared venv is `uv sync`. Check the directory into a dotfiles repo or a private GitHub repo, clone it onto a new machine, run `uv sync`, and the Python version and dependencies match exactly. The shared-venv pattern gives you neither the pin file nor the reproducible rebuild.

The trade-off is that running a script requires `cd`-ing into the directory or passing the project path. A one-line shell wrapper smooths that over:

```sh
myscript() {
    name=$1; shift
    uv run --project ~/scripts -- python ~/scripts/"$name".py "$@"
}
```

```powershell
function myscript {
    param([string]$name)
    uv run --project "$HOME\scripts" -- python "$HOME\scripts\$name.py" @args
}
```

## Install packaged CLI tools with `uv tool`

A different category of "script" is the published CLI: [`ruff`](https://pydevtools.com/handbook/reference/ruff.md), [`prek`](https://pydevtools.com/handbook/reference/prek.md), `httpie`, `yt-dlp`, [`black`](https://pydevtools.com/handbook/reference/black.md). These are packaged projects distributed on PyPI with proper entry points. `uv tool install httpie` builds an isolated venv for httpie and installs its binary into uv's tool directory (`~/.local/bin` on macOS and Linux; a uv-managed location on Windows). Upgrading one tool never affects another.

`uv tool install` replaces [pipx](https://pydevtools.com/handbook/reference/pipx.md), `brew install`, and `winget install` for Python CLIs. On a fresh setup, run `uv tool update-shell` once so that directory is on your PATH. Reserve PEP 723 and shared-venv patterns for your own ad-hoc scripts; use `uv tool install` for tools someone else packaged.

## Skip `uv pip install --system`

uv accepts a `--system` flag that installs packages into whichever Python interpreter is on your PATH. On macOS that's typically Homebrew's Python or the Xcode Command Line Tools stub; on Linux it's `apt`'s or `dnf`'s Python; on Windows it's the python.org installer or the Microsoft Store build. None of those are a Python you control. Using it recreates the fragility that drove you to uv: the next time your package manager upgrades Python to a new minor version (a `brew upgrade`, an Xcode CLT reinstall, an `apt upgrade`, a Store-app refresh), the packages disappear, and scripts pointing at `python3` or `python.exe` fail with `ModuleNotFoundError`.

`pip install --break-system-packages` exists for the same reason and fails the same way. See [Why should I avoid the system Python?](https://pydevtools.com/handbook/explanation/why-should-i-avoid-system-python.md) and [Should I use Homebrew to install Python?](https://pydevtools.com/handbook/explanation/should-i-use-homebrew-to-install-python.md) for the background on why the Python you inherit from your OS or package manager is not the Python you want to install into.

## Pick the option that fits the script

Match the pattern to what the script actually is:

- One-off script with a few dependencies: PEP 723 inline metadata. Same for any script you want to still run in three years, since it carries its own spec.
- Related personal scripts that move together: a shared venv at `~/.venvs/scripts`.
- Script collection you want to rebuild on another machine: a uv project with `pyproject.toml` and `uv.lock`.
- Published CLI tool on PyPI: `uv tool install`.

## Learn More

- [How to write a self-contained Python script](https://pydevtools.com/handbook/how-to/how-to-write-a-self-contained-script.md)
- [What is PEP 723?](https://pydevtools.com/handbook/explanation/what-is-pep-723.md)
- [What happens when you run `uv run`](https://pydevtools.com/handbook/explanation/what-happens-when-you-run-uv-run.md)
- [Why should I avoid the system Python?](https://pydevtools.com/handbook/explanation/why-should-i-avoid-system-python.md)
- [Should I use Homebrew to install Python?](https://pydevtools.com/handbook/explanation/should-i-use-homebrew-to-install-python.md)
- [uv docs: Running scripts](https://docs.astral.sh/uv/guides/scripts/)
