Skip to content

What is uv and Why Should Python Developers Use It?

Problem

I was starting a new Python project. I ran the usual commands:

terminal
python -m venv .venv
source .venv/bin/activate
pip install requests pandas numpy
python main.py

Then I opened a new terminal tab to run a quick test. My script failed because I forgot to activate the virtual environment. Again.

terminal
$ python main.py
Traceback (most recent call last):
File "main.py", line 1, in <module>
import requests
ModuleNotFoundError: No module named 'requests'

I spent the next 20 minutes debugging why my packages weren’t installed. Turns out I had installed them globally in one terminal while my virtual environment was active in another. Classic Python environment mess.

This happens every single project. I waste 15-30 minutes per project just on environment setup and debugging. Multiply that by dozens of projects per year, and I’m losing hours to something that should just work.

What’s Happening?

Python’s virtual environment system is fundamentally broken for developer workflow:

Traditional Workflow Pain Points
1. Create venv manually: python -m venv .venv
2. Activate in EVERY terminal: source .venv/bin/activate
3. Remember to activate before installing: Forgot? Now packages are global
4. Manage multiple venvs across projects: Where was that venv again?
5. Slow package installation: pip resolves dependencies for minutes
6. Inconsistent environments: "Works on my machine" syndrome

A Reddit thread confirmed I’m not alone. The top comment with 15 upvotes was blunt: “Don’t waste time managing virtual environments. Learn how to use uv. It takes care of everything for you.”

Multiple other developers simply responded: “Use UV.”

The community consensus was clear. There’s a tool that eliminates this entire problem.

The Solution: uv

uv is a modern Python package installer and resolver from Astral. It’s written in Rust and automatically manages virtual environments without any manual activation.

Let me try the basic workflow:

terminal
# Initialize a new project
uv init my-project
# Navigate into project
cd my-project
# Add dependencies - no activation needed
uv add requests pandas numpy
# Run my script - uv handles everything
uv run python main.py

That’s it. Three commands. No manual venv creation. No activation. No forgetting to activate.

Comparison: Traditional vs uv

Let me see the difference clearly:

traditional_vs_uv.sh
# Traditional approach (pip + virtualenv) - Multiple error-prone steps
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install requests pandas numpy
python main.py
# Don't forget to activate in every new terminal session!
# If you forget, packages install globally
# If you have multiple terminals, which one is active?
# uv approach - Three simple commands
uv init my-project && cd my-project
uv add requests pandas numpy
uv run python main.py
# Done. No activation. No manual venv management.

Why uv Works Better

I dug into why uv eliminates the friction:

1. Automatic Virtual Environment Management

terminal
# uv creates and manages .venv automatically
$ uv add requests
Resolved 6 packages in 12ms
Installed 6 packages in 45ms
+ certifi==2024.2.2
+ charset-normalizer==3.3.2
+ idna==3.6
+ requests==2.31.0
+ urllib3==2.2.1

When I run uv add, it automatically creates a virtual environment if one doesn’t exist. No manual python -m venv needed.

2. No Activation Required

terminal
# Open a new terminal? Just run your script
$ uv run python main.py
# uv automatically uses the correct virtual environment

The uv run command handles environment activation internally. I never need to remember source .venv/bin/activate.

3. Blazing Fast Package Installation

terminal
# Traditional pip: Resolves and installs over 30 seconds
$ pip install pandas
Collecting pandas
# ... waiting ...
Successfully installed pandas-2.2.0
# uv: Same packages in under 1 second
$ uv add pandas
Resolved 8 packages in 89ms
Installed 8 packages in 312ms

uv is written in Rust. It’s 10-100x faster than pip for dependency resolution and installation.

4. Deterministic Lock Files

terminal
# uv automatically creates uv.lock
$ uv add requests
# Generates uv.lock with exact versions

The uv.lock file ensures everyone on my team has identical environments. No more “works on my machine” problems.

How It Works

Let me trace through what happens:

uv Internal Flow
┌─────────────────────────────────────────────────────────────┐
│ uv add requests │
├─────────────────────────────────────────────────────────────┤
│ 1. Check if .venv exists │
│ └─ If not, create it automatically │
│ │
│ 2. Resolve dependencies with SAT solver (Rust-based) │
│ └─ Much faster than pip's backtracking │
│ │
│ 3. Download and install packages │
│ └─ Parallel downloads, efficient caching │
│ │
│ 4. Update uv.lock file │
│ └─ Exact versions for reproducibility │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ uv run python main.py │
├─────────────────────────────────────────────────────────────┤
│ 1. Locate or create virtual environment │
│ │
│ 2. Activate environment internally │
│ │
│ 3. Execute command with correct Python and packages │
│ │
│ 4. Done - no deactivation needed │
└─────────────────────────────────────────────────────────────┘

The key insight: uv treats virtual environment management as an implementation detail, not a user responsibility.

Working with Development Dependencies

uv also handles dev dependencies cleanly:

terminal
# Add testing and linting tools as dev dependencies
uv add --dev pytest black ruff
# Run tests - uv handles environment automatically
uv run pytest
# Format code
uv run black .
# Lint code
uv run ruff check .

Dev dependencies are tracked separately in pyproject.toml:

pyproject.toml
[project]
name = "my-project"
version = "0.1.0"
dependencies = [
"requests>=2.31.0",
"pandas>=2.2.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"black>=24.0.0",
"ruff>=0.2.0",
]

Running Scripts with Inline Dependencies

uv supports PEP 723 inline script metadata:

main.py
# /// script
# requires-python = ">=3.11"
# dependencies = ["requests", "pandas"]
# ///
import requests
import pandas as pd
def fetch_data():
response = requests.get("https://api.example.com/data")
return pd.DataFrame(response.json())
if __name__ == "__main__":
df = fetch_data()
print(df.head())
terminal
# Run directly - uv handles everything
$ uv run main.py
# No venv setup, no pip install, just works

Migrating from pip to uv

For existing projects:

terminal
# Option 1: Install from requirements.txt
uv pip install -r requirements.txt
# Option 2: Let uv manage everything
uv add $(cat requirements.txt)
# Option 3: Initialize in existing project
cd existing-project
uv init
uv add $(pip freeze | cut -d= -f1)

Common Mistakes When Transitioning to uv

I made these mistakes when I first started:

Mistake 1: Trying to Manually Activate Virtual Environments

terminal
# WRONG - Not needed with uv
$ source .venv/bin/activate
$ pip install requests

This defeats the purpose of uv. The correct approach:

terminal
# CORRECT - Let uv handle everything
$ uv add requests
$ uv run python main.py

Mistake 2: Mixing uv with pip

terminal
# WRONG - Using pip in a uv project
$ uv add requests
$ pip install pandas # This bypasses uv's tracking!

uv can install from PyPI just like pip. Use uv exclusively:

terminal
# CORRECT - Use uv for all packages
$ uv add requests pandas

Mistake 3: Not Committing Lock Files

terminal
# WRONG - Ignoring uv.lock
$ git add .
$ git commit -m "Add dependencies"
# .gitignore: uv.lock <-- Don't do this!

Lock files ensure reproducibility. Always commit them:

terminal
# CORRECT - Commit uv.lock for reproducible builds
$ git add pyproject.toml uv.lock
$ git commit -m "Add dependencies with lock file"

Mistake 4: Assuming uv is Only for New Projects

uv works great in existing projects:

terminal
# Migrate existing project
cd my-existing-project
uv init # Creates pyproject.toml
uv add $(pip freeze | cut -d= -f1) # Add all current packages

Mistake 5: Ignoring Performance Benefits

I used to think “pip is fast enough.” Then I timed it:

benchmark.sh
# pip timing
$ time pip install pandas numpy scipy matplotlib
# real: 47 seconds
# uv timing
$ time uv add pandas numpy scipy matplotlib
# real: 2.1 seconds

In CI/CD pipelines, this 45-second saving per build adds up to hours saved per month.

Why This Matters

After using uv for a month, I’ve noticed real improvements:

Time Savings Per Project
Traditional workflow: 15-30 minutes environment setup
uv workflow: 1-2 minutes
Projects per month: ~10
Time saved per month: ~4 hours

More importantly, I’ve eliminated entire categories of errors:

Errors I No Longer Make
1. Installing packages globally instead of in venv
2. Forgetting to activate venv before pip install
3. Different package versions between dev and prod
4. "Works on my machine" debugging sessions
5. CI/CD failures due to environment mismatches

The mental overhead of virtual environment management is completely gone. I think about code, not environment setup.

The Reason uv Exists

Astral (the company behind uv) recognized that Python’s tooling friction was driving developers away. The virtual environment system made sense in 2004. In 2024, it’s unnecessary cognitive overhead.

uv’s design philosophy:

uv Design Principles
1. Virtual environments are implementation details, not user concerns
2. Dependency resolution should be fast and deterministic
3. One tool should handle everything (install, run, manage)
4. Developers should focus on code, not tooling

This aligns with how modern language tools work. Rust has cargo. Node has npm. Go has go mod. Python finally has uv.

Summary

uv eliminates Python virtual environment complexity through automatic management, 10-100x faster performance than pip, and a simple three-command workflow:

quickstart.sh
uv init my-project && cd my-project # Create project
uv add requests pandas numpy # Add dependencies
uv run python main.py # Run code

The key insight is that virtual environment management should be invisible. When I install a package, I want it available. When I run a script, I want it to work. I shouldn’t need to remember activation commands or debug environment issues.

After a month of using uv, I’ve reclaimed hours previously lost to environment debugging. More importantly, my development workflow is simpler. I think about code, not tooling.

Install uv with:

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

Then try uv init in your next project. The first time you run uv run python script.py without ever typing source .venv/bin/activate, you’ll understand why developers are switching.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments