Step 8 · Ubuntu · Independent
Packages and projects, with uv
uv is a single fast binary that installs Python itself, creates environments, resolves dependencies and runs your code. It replaces pip, venv, virtualenv, pyenv, pipx and poetry — all of them.
Step 4 already gave you
python3, which is everything you need to write a script and
run it. Come here when a script needs a library, or
when you want a Python version other than the one Ubuntu ships. It is not
part of the install_all.sh run.
Run this
$ curl -fsSL https://aikaryashala.com/system_setup/scripts/install_uv.sh | bash
Then reload your shell so uv is on your PATH:
$ source ~/.bashrc
$ uv --version
Why uv, and not the tools you have read about
Python's packaging story accumulated a lot of separate tools over twenty
years. Each solved one problem: venv isolates dependencies,
pip installs them, pyenv manages interpreter
versions, pipx installs applications, poetry locks
versions. Learning them is mostly learning how they fail to cooperate.
uv does all of it, in one tool, typically in under a second.
| Instead of | Use |
|---|---|
python3 -m venv .venv && source .venv/bin/activate | uv venv — or nothing at all; uv run handles it |
pip install requests | uv add requests |
pip install -r requirements.txt | uv sync |
pip freeze > requirements.txt | nothing — uv.lock is maintained for you |
pyenv install 3.14 | uv python install 3.14 |
pipx install ruff | uv tool install ruff |
pipx run ruff check . | uvx ruff check . |
python script.py | uv run script.py |
Ubuntu itself is written partly in Python, and the
/usr/bin/python3 from step 4 belongs to the operating system.
Installing packages there can break system tools. Modern Ubuntu refuses
outright, with an externally-managed-environment error.
uv sidesteps this entirely: it downloads its own Python builds into your home directory and never touches the system one.
A script with its own dependencies
A standalone script can declare what it needs in a comment block at the top. uv reads it, builds a throwaway environment, and runs the script — nothing to install first, nothing left behind.
# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx"]
# ///
import httpx
response = httpx.get("https://api.github.com/repos/aikaryashala/system_setup")
print(response.json()["full_name"])
$ uv run fetch.py
Or let uv write that header for you:
$ uv add --script fetch.py httpx
Starting a project
$ uv init myproject
$ cd myproject
$ uv add requests
$ uv run main.py
That leaves you with:
myproject/
├── .python-version which Python this project uses
├── pyproject.toml your dependencies, declared
├── uv.lock exact resolved versions - commit this
├── README.md
├── main.py
└── .venv/ the environment, managed for you - do not commit
uv run finds the project's environment, creates it if needed,
installs anything missing, and then runs your command inside it. Forgetting
to activate an environment stops being a category of mistake you can make.
Everyday project commands
uv add httpxAdd a dependency, update the lock file, install ituv add --dev pytestAdd a development-only dependencyuv remove httpxRemove oneuv syncMake the environment match the lock file exactlyuv lock --upgradeUpdate dependencies to their newest allowed versionsuv run pytestRun a command inside the project environmentuv treeShow the dependency treeuv run --python 3.12 main.pyRun under a different Python, just this once
Debugging a uv project
The pdb skills from step 12 carry over
unchanged — put uv run in front:
$ uv run python -m pdb main.py
$ uv run python -m pdb -c continue main.py
A breakpoint() in your source works the same way under uv run main.py.
Managing Python versions
# install a specific version
$ uv python install 3.14
# what is installed, and what is available
$ uv python list
# pin this project to a version - writes .python-version
$ uv python pin 3.14
# where does that interpreter actually live?
$ uv python find 3.14
Installing several versions side by side is normal and cheap. Testing against
an older Python is uv run --python 3.11 pytest.
Command line tools
Some Python packages are applications rather than libraries. Install those globally, each isolated in its own environment:
# install permanently, onto your PATH
$ uv tool install ruff
# run once without installing anything
$ uvx ruff check .
$ uvx ruff format .
# manage what you have installed
$ uv tool list
$ uv tool upgrade --all
uvx is the one to remember. It downloads, caches and runs a tool
in one step, which makes trying something out essentially free.
An existing project that uses requirements.txt
uv reads the old formats, so you can adopt it without rewriting anything:
# create an environment and install from requirements.txt
$ uv venv
$ uv pip install -r requirements.txt
# or convert properly, to pyproject.toml and a lock file
$ uv init --bare
$ uv add -r requirements.txt
uv pip
uv pip exists only as a compatibility layer for projects that
still expect pip's interface. For anything new, use uv add and
uv sync — they maintain a lock file, which uv pip
does not.
Check it worked
$ uv --version
$ uv python list --only-installed
$ uv run --no-project python -c "import sys; print(sys.version)"
uv installs into ~/.local/bin. The script adds that directory
to your PATH, but your current shell has not read the change
yet. Run source ~/.bashrc, or open a new terminal.