# Seamless Python Environment Management on macOS

*February 18, 2026*
 — by Flaviu Vlaicu

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



# 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

```bash
brew install uv direnv
```

### Step 2 — Hook direnv into zsh

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

### Step 3 — Create the global direnv helper

```bash
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

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

### Step 5 — Add `.envrc` to global gitignore

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

This prevents `.envrc` files from cluttering your repositories.

### Step 6 — Reload your shell

```bash
source ~/.zshrc
```

## Usage

### Starting a New Project

```bash
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

```bash
# 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:

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

### Disabling Auto-Activation

```bash
# 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.

```bash
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:

```bash
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:

```bash
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

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


---
*Source: [https://vlaicu.io/posts/uv-direnv/](https://vlaicu.io/posts/uv-direnv/)*
