Skip to content

Virtual Environments with uv

Install uv

curl -sSf https://astral.sh/uv/install.sh | sh

Migrating existing projects from venv to uv

# 1. Remove the existing environment
rm -rf .venv

# 2. (Optional) Clear the uv cache if you suspect corruption
uv cache clean

# 3. Create a fresh environment and install dependencies 
# from your requirements file in one shot
uv venv
uv pip install -r requirements.txt

# 4. Lock it into the project management system
uv sync

Note

Step 3 can also include a specific version of Python as follows:

uv venv --python=3.12 .venv

Switching from pyenv to uv

uv has become the "standard" for Python development because it consolidates the entire stack into a single, blazingly fast Rust-based binary.

If you switch to uv, you can essentially delete pyenv, pip, virtualenv, and pip-tools from your brain. Here is how it replaces your current workflow.

  1. Replacing pyenv (Version Management)

With pyenv, you had to manually install versions and manage shims. With uv, you just ask for a version, and it fetches a pre-compiled, high-performance binary in seconds (not minutes).

Feature pyenv Command uv Equivalent
Install version pyenv install 3.12.9 uv python install 3.12.9
List available pyenv install --list uv python list
Pin version pyenv local 3.12.9 uv python pin 3.12.9

The "Magic" Difference: You don't even need to install Python first. If you run uv run --python 3.13 main.py, and 3.13 isn't on your machine, uv will download it, create a temporary environment, and run your code all in one go.

  1. Replacing pip and venv (Project Management)

uv replaces the "create-activate-install" dance with a single, unified project structure.

The Old Way (pip + venv):

python -m venv .venv
source .venv/bin/activate
pip install requests
pip freeze > requirements.txt

The New Way (uv):

uv init             # Creates pyproject.toml and .python-version
uv add requests     # Installs requests AND updates your lockfile/toml automatically
uv run main.py      # Automatically handles the virtual environment for you
  • Speed Check: uv is consistently 10x to 100x faster than pip. On a warm cache, installing a library like pandas is nearly instantaneous (sub-second) because it uses a global cache and "reflink" technology to avoid copying files.

  • Replacing pipx (Global Tools)

You likely use pipx for tools like ruff, black, or yt-dlp. uv replaces this with the tool command.

  • To run a tool once (ephemeral): uvx ruff check
  • To install a tool globally: uv tool install yt-dlp

  • How to Migrate Right Now

If you want to move your existing setup to uv:

Install uv:

curl -LsSf https://astral.sh/uv/install.sh | sh

Initialize your project:

# Inside your project folder, run 
uv init

It will detect your requirements.

Sync everything:

uv sync

The Bottom Line

By switching to uv, you no longer have to worry about your $PATH or whether /usr/local/bin is interfering. uv manages its own isolated versions of Python in ~/.local/share/uv and keeps your system pristine.

Moving from pyenv to uv is like trading in a garage full of specialized tools for a single, high-tech Swiss Army knife. Before we run the script, we need to ensure we don't accidentally wipe out any project data.

Here is a safe, step-by-step migration plan:

  1. Install uv

First, get the engine running before we take the old one apart.

curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.cargo/env  # Loads uv into your current session
  1. The "Safe Migration" Script

You can run these commands manually or save them as a script. This process will identify your current pyenv versions, install them via uv, and then clean up the old configurations.

# 1. Grab your current global version from pyenv
CURRENT_PY=$(pyenv global)

# 2. Tell uv to install that version so you aren't left stranded
uv python install $CURRENT_PY

# 3. Open your ~/.zshrc to remove the old pyenv block

# Look for the lines we added earlier:
# export PYENV_ROOT...
# eval "$(pyenv init -)"
# DELETE or COMMENT THEM OUT.

nano ~/.zshrc

# 4. Refresh your shell
source ~/.zshrc
  1. Transitioning your Projects

For every project folder you currently have, follow this "Uv-ify" workflow:

  • Enter the project: cd my-project
  • Initialize uv: uv init
  • Migrate requirements: If you have a requirements.txt, run:
uv add -r requirements.txt
  • Delete the old .venv: rm -rf .venv (uv will manage its own optimized environment automatically when you run commands).

  • The "Nuclear" Cleanup (Optional)

Once you are confident that uv is handling your versions correctly, you can remove the pyenv data to save several gigabytes of disk space:

rm -rf ~/.pyenv
brew uninstall pyenv

Why this is the "Final" Move

With uv, your system state now looks like this:

Component Logic
Python Binaries Managed by uv in ~/.local/share/uv/python
Global Tools Managed by uv tool (isolated and fast)
Projects Defined by pyproject.toml and uv.lock (reproducible)
Performance No "shims" slowing down every command execution

A Quick Tip for the Road

Since uv is so fast, you can now run scripts with specific dependencies without even having a project folder. Try this:

uv run --with requests --with beautifulsoup4 my_scraper.py

It will build a temporary environment, run the script, and clean up—all in the time it used to take pip just to search for the package.

Even if you don't have Rust or Cargo installed, the uv installer (which is made by the same folks who built the Ruff linter) follows the Rust convention of placing its binaries and environment scripts into a folder named .cargo in your home directory.

When you run that install script, it creates that folder specifically to house the uv binary. You don't need the full Rust toolchain; you just need that one path script to tell your terminal where uv lives.

If that folder doesn't exist yet...

If the command fails because the directory truly isn't there, it means the installer didn't finish or you might prefer the Homebrew way (which avoids the .cargo folder entirely).

The "No-Cargo" Way (Recommended for you): Since we've been using Brew for everything else, let's keep it consistent. This avoids the $HOME/.cargo/env requirement entirely:

brew install uv

Once installed via Brew, uv is automatically added to your path in /opt/homebrew/bin/uv, and you can skip that source command entirely.

Setting up VS Code with uv

Now that uv is your "one source of truth," you'll want VS Code to stop looking for the old pyenv or /usr/local/bin versions.

  1. Install the "Python" Extension: (You likely already have this)
  2. Open your Project: Open a folder where you've run uv init
  3. Select Interpreter:

  4. Press Cmd + Shift + P

  5. Type "Python: Select Interpreter"

uv creates environments in a folder named .venv inside your project. VS Code should automatically detect this and labeled it as (.venv): uv.

Pro Tip: The "Zero Config" VS Code Setup

If you want VS Code to always use uv without you having to click anything, add this to your project's .vscode/settings.json:

{
  "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
  "python.terminal.activateEnvInSelectedTerminal": true
}

Summary of your new "Clean" Machine

  • Version Manager: uv (Fast, Rust-based)
  • Package Manager: uv (Replaces pip)
  • Environment Manager: uv (Replaces venv/virtualenv)
  • Global Tool Manager: uv tool (Replaces pipx)

"Lockfile" Workflow

This is the secret to making sure your code runs exactly the same way on your computer as it would on a teammate's or a server.

  1. Remove the "Curl" Version

Since the installer just placed a folder in your home directory, we can delete it and remove the path reference it added to your shell.

  • Delete the folder:
rm -rf ~/.cargo
  • Clean your .zshrc: Open your config file: nano ~/.zshrc.
  • Look for a line at the bottom that looks like source $HOME/.cargo/env.
  • Delete that line.
  • Refresh:
source ~/.zshrc
  1. Install via Brew

Now, let's do it the way you preferred. This will put the uv binary in /opt/homebrew/bin, which is already in your $PATH.

brew install uv
  1. Verify the Switch

Run this to make sure you're using the Brew-managed version:

which uv

Expected result: /opt/homebrew/bin/uv (or /usr/local/bin/uv on Intel Macs).

Why Brew is actually better here

By using Brew, you get a few nice perks:

  • Automatic Updates: When you run brew upgrade, uv gets updated along with everything else.
  • Centralized Management: You don't have to remember to check a hidden .cargo folder for updates.
  • No Manual PATH Edits: Brew handles the linking for you.

What about my Python versions?

Don't worry — uv stores the Python versions it downloads in ~/.local/share/uv. Removing the .cargo folder does not delete the Python versions you may have already installed via uv. They will still be there and ready to use once you reinstall uv via Brew.

Setting a global default with uv is slightly different than pyenv. Because uv focuses on speed and avoiding the "magic" of shims, it doesn't try to hijack your system's python3 command unless you tell it to.

Here is how to set up your "one version to rule them all."

  1. Set the Global Python Version

Run this command to pick your preferred version (e.g., 3.13):

uv python install 3.13

Now, tell uv to use this as the default whenever you aren't inside a specific project:

uv python pin 3.13 --global
  1. The "Last Step" for Convenience

By default, uv wants you to run things via uv run python. If you want to be able to just type python3 in your terminal and have it use the uv version, you should add one final alias to your ~/.zshrc:

  • Open your config: nano ~/.zshrc
  • Add this to the bottom:
# Use uv's managed Python as the primary python3 command
alias python3="uv run python"
  • Save and Refresh: source ~/.zshrc

  • How your System is now Structured

You've successfully moved from a cluttered mess to a streamlined, modern stack.

Command What it does now
python3 Uses the alias to run the uv version (fast & clean)
uv add Installs a library into a local project instantly
uv tool install Installs a global CLI tool (replaces pipx)
uv python install Fetches any version of Python in seconds (replaces pyenv)

Final Clean-Up Check

Since we just did a lot of "surgery" on your .zshrc, make sure you don't have any leftover export PATH lines from our earlier attempts with Homebrew Python or pyenv. Your .zshrc should now be very short and clean!

If you haven't actually started using pyenv to manage projects yet, you’ve caught this at the perfect time. You are essentially standing in a clean room with a box of tools you haven't opened yet.

The "migration" is less about moving data and more about unplugging the old pipes before you install the new system.

  1. Remove the pyenv "Plumbing"

Since you haven't used it, pyenv hasn't created any virtual environments or shim dependencies yet. You just need to stop your shell from looking for it.

  • Uninstall the package:
brew uninstall pyenv
  • Clean your .zshrc: Open the file: nano ~/.zshrc. Remove these lines (the ones we added in the previous steps):
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
  • Delete the hidden data folder: Even after uninstalling the program, pyenv leaves a folder in your home directory. Wipe it out:
rm -rf ~/.pyenv
  1. Install uv (The Brew Way)

Now, install your new all-in-one tool:

brew install uv
  1. Initialize your First Project

Since you’re starting fresh, here is the uv workflow for a brand-new project. Notice how you never have to "create" a virtual environment manually—uv handles it the moment you add a package.

mkdir my-new-project
cd my-new-project

# 1. Initialize the project (creates pyproject.toml)
uv init

# 2. Add a package (uv automatically creates the .venv and installs it)
uv add requests

# 3. Run your script
uv run main.py

How uv handles Python versions without pyenv

In the pyenv world, you had to run pyenv install 3.12.0 before you could use it. With uv, if your pyproject.toml says you need Python 3.12, but you don't have it installed, uv will simply say:

"Python 3.12 not found. Downloading..."

It fetches the version, keeps it in a hidden shared folder (~/.local/share/uv), and links it to your project. You never have to manually manage "installed" versions again.

Summary of your current state

Because you hadn't started using pyenv, you have zero technical debt. Your system is now:

  • Clean: No shims, no complex $PATH overrides.
  • Fast: uv is written in Rust and operates directly on your binaries.
  • Simple: One tool (uv) replaces four (pyenv, pip, venv, pip-tools).