uv + direnv

A manual, lightweight approach to Python virtual environment management that auto-activates when you cd into a project and deactivates when you leave — without ever running source .venv/bin/activate again.

Why This Approach?

Traditional Python workflows require manually activating and deactivating virtual environments. Forget to activate? You install packages globally. Forget to deactivate? You pollute one project with another’s dependencies. This setup eliminates that entire class of mistakes.

uv is a blazing-fast Python package manager written in Rust. It replaces pip, pip-tools, virtualenv, and pyenv in a single binary. direnv is a shell extension that loads and unloads environment variables (including venv activation) based on the current directory.

Together, they give you:

  • Instant venv activation on cd — no manual steps
  • Automatic deactivation when you leave the directory
  • Sub-second package installs via uv’s Rust-powered resolver
  • Per-project isolation with zero friction
  • Full control — you choose which projects opt in

Prerequisites

  • macOS with Homebrew installed
  • zsh (default shell on macOS)

Installation

Step 1 — Install uv and direnv

brew install uv direnv

Step 2 — Hook direnv into zsh

echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc

Step 3 — Create the global direnv helper

mkdir -p ~/.config/direnv

cat << 'EOF' > ~/.config/direnv/direnvrc
use_uv() {
    if [ ! -d .venv ]; then
        uv venv
    fi
    source .venv/bin/activate
}
EOF

This defines a reusable use_uv function available to all your projects.

Step 4 — Add the convenience alias

echo 'alias uvenv='"'"'echo "use_uv" > .envrc && direnv allow'"'"'' >> ~/.zshrc

Step 5 — Add .envrc to global gitignore

mkdir -p ~/.config/git
echo ".envrc" >> ~/.config/git/ignore

This prevents .envrc files from cluttering your repositories.

Step 6 — Reload your shell

source ~/.zshrc

Usage

Starting a New Project

mkdir ~/myproject && cd ~/myproject
uv init          # creates pyproject.toml and project structure
uvenv            # enables auto-activation for this directory
uv add requests  # install packages

Day-to-Day Workflow

# cd in — activates automatically
cd ~/myproject
# direnv: loading ~/myproject/.envrc
# direnv: export +VIRTUAL_ENV +VIRTUAL_ENV_PROMPT ~PATH

python main.py   # uses the project's venv python

# cd out — deactivates automatically
cd ~
# direnv: unloading

Verifying It Works

When entering a project directory, you’ll see direnv output:

direnv: loading ~/myproject/.envrc
direnv: export +VIRTUAL_ENV +VIRTUAL_ENV_PROMPT ~PATH

You can also verify with:

which python
# Active:     /Users/you/myproject/.venv/bin/python
# Not active: /usr/bin/python3

Disabling Auto-Activation

# Temporarily stop auto-activation for a directory
direnv deny

# Re-enable it
direnv allow

# Permanently remove — delete the .envrc file
rm .envrc

Quick Reference

TaskCommand
Create a new projectuv init
Enable auto-activationuvenv
Add a packageuv add <package>
Remove a packageuv remove <package>
Run without activatinguv run python script.py
Disable auto-activationdirenv deny
Re-enable auto-activationdirenv allow
Update all packagesuv lock --upgrade && uv sync
Check active environmentwhich python

Use Cases

Multi-Project Development

Switch between projects with different dependencies seamlessly. Each project has its own isolated venv that activates the moment you enter the directory.

cd ~/project-a    # Python 3.12, Flask app
cd ~/project-b    # Python 3.11, Django app
cd ~/project-c    # Python 3.13, CLI tool

No crossed wires, no stale environments.

Team Onboarding

New team members clone a repo and run two commands:

git clone <repo>
cd <repo>
uvenv
uv sync

The environment is ready. No README steps to forget, no “works on my machine” issues.

Quick Scripting

Need a throwaway project for a one-off script? The overhead is near zero:

mkdir /tmp/scratch && cd /tmp/scratch
uv init
uvenv
uv add pandas matplotlib
python analysis.py

Monorepo / Multi-Service

Each service directory gets its own .envrc and venv. Navigating between services swaps environments automatically:

monorepo/
├── api/          # .envrc → FastAPI + SQLAlchemy
├── worker/       # .envrc → Celery + Redis
└── scripts/      # .envrc → lightweight CLI tools

How It Works

  1. direnv hooks into your shell’s cd command via chpwd
  2. When you enter a directory with an .envrc file, direnv executes it
  3. The use_uv function checks for a .venv directory and creates one with uv venv if missing
  4. It then sources .venv/bin/activate, setting PATH and VIRTUAL_ENV
  5. When you leave, direnv restores the original environment variables

The key insight: direnv doesn’t just set variables — it tracks what changed and reverses everything on exit. Your shell is always clean.

Files Created

FilePurpose
~/.config/direnv/direnvrcGlobal use_uv helper function
~/.config/git/ignoreKeeps .envrc out of all repos
<project>/.envrcPer-project opt-in (created by uvenv)
<project>/.venv/Virtual environment (created automatically)

Troubleshooting

direnv doesn’t load when I cd in Run direnv allow — direnv blocks untrusted .envrc files by default.

“command not found: uv” Ensure Homebrew’s bin is in your PATH: eval "$(/opt/homebrew/bin/brew shellenv)"

Wrong Python version Specify the version when creating the venv: edit .envrc to use uv venv --python 3.12

Slow shell startup direnv’s hook is negligible. If your shell is slow, check other plugins.