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
| Task | Command |
|---|---|
| Create a new project | uv init |
| Enable auto-activation | uvenv |
| Add a package | uv add <package> |
| Remove a package | uv remove <package> |
| Run without activating | uv run python script.py |
| Disable auto-activation | direnv deny |
| Re-enable auto-activation | direnv allow |
| Update all packages | uv lock --upgrade && uv sync |
| Check active environment | which 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
- direnv hooks into your shell’s
cdcommand viachpwd - When you enter a directory with an
.envrcfile, direnv executes it - The
use_uvfunction checks for a.venvdirectory and creates one withuv venvif missing - It then sources
.venv/bin/activate, settingPATHandVIRTUAL_ENV - 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
| File | Purpose |
|---|---|
~/.config/direnv/direnvrc | Global use_uv helper function |
~/.config/git/ignore | Keeps .envrc out of all repos |
<project>/.envrc | Per-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.