Duncan Leung
Setting Up Python with uv
Published on

Setting Up Python with uv

Authors

A few years ago I wrote about setting up Python with pyenv, pyenv-virtualenv, and Poetry. That stack worked, but it was three separate tools stitched together: one for Python versions, one for virtual environments, and one for packages.

uv collapses all three into a single tool. It is built in Rust by Astral (the same team behind the Ruff linter), and over the last ~18 months it has become the de-facto industry standard for Python tooling. It is also dramatically faster — installs that took Poetry seconds finish in milliseconds.

This is a guide to install and set up Python with uv on a Mac.

Skip to the sections below:

Additional reference: uv Documentation

Install uv

Docs: uv - Installation

uv is a single static binary with no Python dependency of its own, so it manages Python rather than relying on it.

Install with the standalone installer:

$ curl -LsSf https://astral.sh/uv/install.sh | sh

Or with Homebrew:

$ brew install uv

Update uv

If you installed with the standalone installer, update in place:

$ uv self update

If you installed with Homebrew, use brew upgrade uv instead.

Enable shell completions

Add uv completions to your shell.

.zshrc
eval "$(uv generate-shell-completion zsh)"

Restart your shell so the changes take effect.

$ exec "$SHELL"

Python Version Management

Docs: uv - Installing Python

This replaces pyenv. uv downloads and manages standalone Python builds for you — there is no need to compile from source, so the zlib / Xcode header issues from the old pyenv workflow disappear.

# List versions available to install
$ uv python list

# Install a specific version
$ uv python install 3.13

# Install multiple versions at once
$ uv python install 3.11 3.12 3.13

# Find where an installed version lives
$ uv python find 3.13

# Uninstall a version
$ uv python uninstall 3.11

Pin a project to a Python version

uv python pin writes a .python-version file to the current directory, similar to pyenv local. uv auto-selects that version for commands run in the directory.

$ uv python pin 3.13
.python-version
3.13

You usually do not need to set a "global" version. uv resolves the right Python per project from .python-version and the requires-python field in pyproject.toml, and downloads it on demand if it is missing.

Project Workflow

Docs: uv - Working on Projects

This replaces Poetry. uv uses the standard pyproject.toml for metadata and dependencies, and writes a uv.lock file as the snapshot of the exact resolved package set — direct dependencies and their sub-dependencies.

Initialize a new project

$ uv init my-project
$ cd my-project

This scaffolds a pyproject.toml, a .python-version, a README.md, and a starter main.py.

Add and remove dependencies

uv add resolves, installs, updates pyproject.toml, and updates uv.lock in one step. The first add also creates the project's virtual environment in .venv automatically.

# Add the requests package and its dependencies
$ uv add requests

# Add a dev-only dependency
$ uv add --dev pytest

# Pin a version constraint
$ uv add 'requests>=2.31'

# Remove a package and its now-unused sub-dependencies
$ uv remove requests

Run code in the project environment

uv run executes a command inside the project's environment, syncing dependencies first if anything is out of date. You do not need to manually activate the virtual environment.

# Run a script
$ uv run main.py

# Run an installed tool, e.g. the test suite
$ uv run pytest

Sync and lock

uv keeps the lockfile and environment in sync automatically on add / remove / run. You can also run them explicitly — useful in CI or after a fresh git clone.

# Resolve dependencies and write uv.lock
$ uv lock

# Install the locked dependencies into .venv
$ uv sync

Virtual Environments

Docs: uv - Virtual Environments

For project work, uv manages .venv for you and you rarely touch it directly. When you do need a standalone environment, uv venv replaces virtualenv / pyenv-virtualenv.

# Create a virtual environment in .venv
$ uv venv

# Create one with a specific Python version
$ uv venv --python 3.13

# Activate it
$ source .venv/bin/activate

# Deactivate it
$ deactivate

Check the installation

With the environment active, which python should resolve to the project's .venv.

$ which python
  /Users/MACHINE_NAME/my-project/.venv/bin/python
$ python --version
  Python 3.13.0

Running Tools: uvx

Docs: uv - Tools

uvx runs a command-line tool in a temporary, isolated environment without installing it into your project — handy for one-off invocations of things like ruff, black, or httpie.

# Run ruff once, in a throwaway environment
$ uvx ruff check

# Run a specific version
$ uvx ruff@0.6.0 check

To install a tool user-wide so it is always on your PATH:

# Install a tool globally for the user
$ uv tool install ruff

# List installed tools
$ uv tool list

# Uninstall a tool
$ uv tool uninstall ruff

Coming from pyenv / Poetry

If you are migrating from the older stack, here is the rough mapping:

Old workflowuv equivalent
pyenv install 3.13uv python install 3.13
pyenv local 3.13uv python pin 3.13
pyenv virtualenv 3.13 my-envuv venv
pyenv activate my-envsource .venv/bin/activate
poetry init / poetry newuv init
poetry add requestsuv add requests
poetry add --dev pytestuv add --dev pytest
poetry remove requestsuv remove requests
poetry installuv sync
poetry lockuv lock
poetry run pytestuv run pytest

The biggest mental shift: with uv you no longer manage the version manager, the virtual environment, and the package manager as separate tools. uv is all three, and it is fast enough that syncing the environment on every run is effectively free.