// CRT MODE ACTIVATED · ↑↑↓↓←→←→BA to toggle
← Writing
Essay

I was a Python environment victim (and then uv showed up)

October 23, 20257 min readbeginner
uvpythonpackaging

uv ate my entire toolchain and I'm not even mad about it.

I have a confession. I've been writing Python for over a decade, and up until last year I still didn't trust my own virtual environments. Every new project started with a small ritual of dread: which Python is active right now? Did I activate the venv? Did I accidentally pip install to my system Python again? Why does conda need 2 GB before I've installed anything?

The Python packaging story has been a running joke in our community for years. We had pip, then virtualenv, then pipenv, then poetry, then conda, then pyenv, then pip-tools, then hatch, then pdm. And somehow the experience kept getting more fragmented with each one.

Then a tool called uv showed up. And it cheated.

The old way was held together with tape

Here's what a typical Python project setup looked like before uv.

pyenv install 3.12.3
pyenv local 3.12.3
python -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
# oops, requirements.txt is outdated
pip freeze > requirements.txt
# wait, that pinned every sub-dependency, not just the things I chose
# actually forget it, let me use poetry
pip install poetry
poetry init
# poetry's resolver is thinking... and thinking... and thinking...

Did your coffee finish brewing during that last step? Good. Maybe next time.

The thing is, every one of these tools solved a real problem. Pip gave us packages. Virtualenv gave us isolation. Poetry gave us deterministic lock files. Pyenv gave us Python version switching. Conda gave us binaries. But none of them talked to each other, and using them together meant understanding the gaps between them well enough to bridge them manually.

That's not a toolchain. That's a part-time job.

uv walked in and broke the cycle

Astral (the same team behind Ruff) released uv in 2024. I installed it assuming I'd poke around and go back to my usual mess. Instead, the first uv sync genuinely took me by surprise.

uv sync

That's it. One command. It installed the right Python version, created a .venv, resolved all dependencies, and locked them into a uv.lock file. I blinked and it was done.

It's written in Rust, which is the polite way of saying it's rude to every other tool's performance. uv doesn't negotiate with your dependencies. It solves them. Fast.

The new workflow is boring (that's the point)

Here's what my Python workflow looks like now:

| Old Me | New Me | |--------|--------| | python -m venv .venv && source .venv/bin/activate | uv sync | | deactivate && source .venv/bin/activate after installing a new dep | uv add numpy | | pip install to some global Python by accident | doesn't happen, uv is venv-native | | pip freeze > requirements.txt (capturing 47 transitive deps I didn't choose) | uv.lock is generated automatically | | "It works on my machine" | "It works everywhere, it's pinned" |

The uv run command is the one that changed my habits most. Instead of activating a venv, running a command, deactivating, forgetting to reactivate, then wondering why jupyter isn't found:

uv run jupyter lab

It just finds the venv uv created and runs the thing. No activation, no source .venv/bin/activate && prefix, and none of the mental bookkeeping I used to carry around.

uv python pin 3.12.9 locks the exact Python version to the project. Anyone else running uv sync gets that exact build. No more "oh you're on 3.11? That's probably fine but let me check this one edge case." It is not probably fine. It is exactly fine.

uvx is the cheat code nobody talks about enough

My favorite uv feature isn't for projects at all. It's uvx (short for uv tool run). It downloads and runs a tool in a one-off ephemeral environment, then cleans up after itself.

uvx ruff
uvx --with pandas,pyarrow ipython
uvx --from jupyterlab jupyter lab

No pip installing tools globally. No deciding which environment they should live in. No stale installs taking up space. It's like npx for Python, and I honestly use it more than I use my microwave.

Someone sends me a notebook? uvx --from jupyterlab jupyter lab. Need to lint a file I don't have a project for? uvx ruff. Quickly inspect a Parquet file? uvx --with pandas,pyarrow ipython.

I used to feel guilty about skipping virtual environments for one-off scripts. uvx eliminated that guilt. It's the "get out of jail free" card that makes full-venv discipline actually stick. You always have an escape hatch.

What this means beyond my laptop

On my own machine, the effect was immediate: my local development stopped being a constant problem. What surprised me more was what happens when you add more people.

On CI, uv reduces the environment setup from "wait for the poetry resolver to finish" (30+ seconds on a cold cache) to "it's already done" (sub-second for the lock file, a few seconds to install). On a team with multiple operating systems, the lock file is platform-agnostic. The same uv.lock works on macOS, Linux, and Windows with the same resolution.

I've started using uv in production, too. The dev and prod environments genuinely match now, where before they only matched in theory. The Python version is pinned, every dependency is locked, and the install step takes milliseconds, not minutes.

Before uv, I had a documented process for setting up a Python project that was 14 steps long. Nobody followed it. Everyone had their own drift. With uv, the process is two steps: uv sync and uv run. And it works the same on every machine, every time.

The part where I try to convince you

I know how this reads. It sounds like I'm evangelizing. Maybe I am. But I spent years fighting Python environments, fighting pip, fighting conda, fighting the gap between what works locally and what works in CI, fighting the urge to just pip install --user everything and accept the consequences.

uv is the first tool that made all of that go away. Not "mostly go away." Not "go away if you follow these six rules." It just works.

| Tool | Python version mgmt | Package install | Dep resolution | Lock file | Speed | |------|:---:|:---:|:---:|:---:|:---:| | pip + venv | manual | ✓ | slow | none | 🐢 | | poetry | external | ✓ | slow | ✓ | 🐢 | | conda | ✓ | ✓ (separate ecosystem) | slow-ish | ✓ | 🐢🐢 | | pyenv + pip-tools | ✓ | ✓ | slow | ✓ | 🐢 | | uv | ✓ | ✓ | fast | ✓ | 🚀 |

Conda deserves a special mention here because it was the closest to "batteries included" before uv. But conda is also 2+ GB just to exist, uses its own package index (so half the things you want aren't there without conda-forge), and the resolver (while better than pip's) is nowhere near uv's speed.

uv is free, open-source, and installs alongside whatever Python setup you already have. No migration trauma. No rm -rf /usr/bin/python3. You can install it, try it on one project, and keep your old workflow for everything else until you're ready.

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

That's it. Fifteen seconds. You can decide later whether to keep it.

Go install it

I don't write posts like this often. But uv genuinely changed how I work with Python, and I think it will change how you work too. Not because the tool is clever, though it is. Because it removes a problem that should never have been there in the first place.

Python is a great language. The packaging experience around it has been, charitably, a work in progress for 20 years. uv doesn't fix everything. But it fixes the parts that hurt the most, every single day.

Install it. uv sync on your current project. uv run your tests. See if you feel like going back.