Some notes on setting up python for development on OS X. The general pattern applies to Linux as well.
Initial Setup
Install homebrew
brew install coreutils
brew install direnv
brew install uv
brew install ruffKeep brew installed components up to date with brew upgrade.
Add the following to start of ~/.zprofile. These commands enable direnv which can be used to set environment variables based on the current folder context.
eval "$(/opt/homebrew/bin/brew shellenv)"
eval "$(direnv hook zsh)"Add the following to ~/.zshrc to enable shell completion support for uv:
eval "$(uv generate-shell-completion zsh)"
eval "$(uvx --generate-shell-completion zsh)"Restart the shell.
Install the desired pythons, for example:
uv python install 3.11
uv python install 3.14Add the script uv-python-symlink to ~/.local/bin. This helps with creating and managing symlinks to the uv installed python versions.
Run uv-python-symlink:
$ ./uv-python-symlink
/Users/vieglais/.local/bin/python3.11 created.
/Users/vieglais/.local/bin/python3.14 created.
/Users/vieglais/.local/bin/python created.Although tempting, it’s generally not a good idea to use brew installed python unless working specifically on brew apps (e.g. QGIS)1.
I use direnv to assist with context management on the cli. There’s plenty of other tools (mise seems pretty good), I just started using direnv a while back and have had no reason to change. To set things up for typical python projects (with a pyproject.toml in the project root):
Create a file ~/.config/direnv/lib/python_helpers.sh:
##
# use venv
function use_venv() {
# Create or reuse and existing .venv
uv venv --allow-existing .venv
# Activate the virtual environment
source .venv/bin/activate
# Report the setup
echo "Activated $(grealpath -s --relative-to=. $(which python)) $(python --version)"
}
##
# use standard-python
# 1. Loads another .envrc if found searching folders upward
# 2. Loads environemnt variables from .env if found
# 3. Loads .envrc.local if present, can be used to override other settings from 1 and 2
# 4. Call use_venv() to activate a local python environment
# 5. Run uv sync to install python project dependencies
function use_standard-python() {
# https://direnv.net/man/direnv-stdlib.1.html#codesourceupifexists-ltfilenamegtcode
source_up_if_exists
# https://direnv.net/man/direnv-stdlib.1.html#codedotenvifexists-ltdotenvpathgtcode
dotenv_if_exists
# https://direnv.net/man/direnv-stdlib.1.html#codesourceenvifexists-ltfilenamegtcode
source_env_if_exists .envrc.local
use venv
uv sync ${UV_SYNC_OPTS}
}
##
# use local-python
# Same as use_standard-python but does not run uv sync
# Use this for a generic venv or if not wanting to auto install dependencies
function use_local-python() {
# Same as use_standard-python but just for local venv, not projects
source_up_if_exists
dotenv_if_exists
source_env_if_exists .envrc.local
use venv
}In project folders that have python dependency, add a file .envrc with the contents:
# Install all package "extra" dependencies
export UV_SYNC_OPTS="--all-extras"
# Activate the python virtual environment, creating if necessary
use standard-python
# Optionally add the local package path to the PYTHONPATH
# export PYTHONPATH="${pwd}:${PYTHONPATH}"Then when cd’ing to the project folder, the environment will be setup to use that local python virtual environment.
After editing .envrc it is necessary to run direnv allow to flag the changes as valid.
Using a specific python version
The environment variable UV_PYTHON acts the same as the --python command-line argument to uv. Set this in the folder .env, .envrc, or .envrc.local to use a specific version of python in a project. Or set it manually, e.g.:
export UV_PYTHON=3.12
See also: https://docs.astral.sh/uv/reference/environment/
Migrating from poetry
In the project folder, this has worked in all cases so far:
uvx migrate-to-uvGit hooks
Install:
uv tool install pre-commit
To upgrade:
uv tool upgrade pre-commit
Config with a file .pre-commit-config.yaml
Linters and stuff
ruff is much faster than black or pylint. Add it as a pre-commit, e.g.:
.pre-commit-config.yaml:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.11.0
hooks:
# Run the linter.
- id: ruff
types_or: [ python, pyi ]
args: [ --fix ]
# Run the formatter.
- id: ruff-format
types_or: [ python, pyi ]