Setting up Python on OS X

Works for me.

recipe
python
Published

May 8, 2025

Modified

October 8, 2025

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 ruff

Keep 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.14

Add 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.
Note

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.

Note

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-uv

Git 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 ]