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.

This step stands alone

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 ofUse
python3 -m venv .venv && source .venv/bin/activateuv venv — or nothing at all; uv run handles it
pip install requestsuv add requests
pip install -r requirements.txtuv sync
pip freeze > requirements.txtnothing — uv.lock is maintained for you
pyenv install 3.14uv python install 3.14
pipx install ruffuv tool install ruff
pipx run ruff check .uvx ruff check .
python script.pyuv run script.py
Never install packages into the system Python

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
There is no “activate” step

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

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
About 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: command not found”

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.