Python Tooling

I Wish I'd Switched My FastAPI Projects to uv Years Ago

By Abhishek Ghimire5 min read
2 views

I Wish I'd Switched My FastAPI Projects to uv Years Ago

For years my Python workflow was the same ritual everyone knows. Create a virtualenv. Activate it. pip install something. Forget to freeze. Remember three weeks later that requirements.txt is now a work of fiction. Do it again on the server and get a slightly different set of versions because the resolver felt like it that day.

Then I moved my FastAPI projects to uv, and it quietly deleted about four separate tools from my life. This is the post I wish someone had shoved in my face a couple of years ago.

The old pain, stated plainly

A "normal" Python backend setup involved juggling pip, venv, pip-tools or pipenv or poetry (pick your fighter), and a requirements.txt that only loosely described reality. The specific things that wore me down:

  • No real lockfile by default. requirements.txt pins what you decided to pin. Transitive dependencies drift. "Works on my machine" is practically the format's tagline.

  • Slow installs. Spinning up a fresh environment in CI or a Docker build meant sitting through pip resolving and downloading, one wheel at a time.

  • Environment management was manual. python -m venv .venv, activate, hope you're in the right shell, notice you weren't.

  • Tool sprawl. Version manager here, virtualenv tool there, dependency resolver somewhere else. Onboarding a new machine was a checklist.

What uv actually replaces

uv is a single binary (written in Rust) that swallows the whole workflow: dependency resolution, locking, virtualenv creation, installing, running scripts, and even installing Python itself. One tool instead of five.

The moment it clicked for me was realizing I no longer had to think about the virtualenv. uv makes and manages it for me, and every command runs against it automatically.

Migrating an existing FastAPI project

If you have a project with a requirements.txt, the move is genuinely a few minutes.

Initialize uv in the project (this creates pyproject.toml):

uv init

Import your existing dependencies:

uv add -r requirements.txt

That reads your old file, resolves everything properly, writes it into pyproject.toml, and generates a uv.lock — a real, cross-platform lockfile with every transitive dependency pinned by hash. You can now delete requirements.txt and stop lying to yourself.

Add FastAPI and the server the same way going forward:

uv add fastapi uvicorn

Run your app without ever activating anything:

uv run uvicorn app.main:app --reload

That uv run is the part that feels like magic at first. No activate, no remembering which .venv you're in. uv ensures the environment matches the lockfile and runs your command inside it.

To reproduce the exact environment somewhere else — a teammate's laptop, CI, a fresh VPS:

uv sync

It reads uv.lock and builds a byte-for-byte identical environment. This is the thing requirements.txt never reliably gave me.

The Docker payoff

This is where I actually said "why did I wait so long." A lean FastAPI Dockerfile with uv looks like this:

FROM python:3.12-slim

# Copy the uv binary from the official image
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /app

# Copy only lock + manifest first so layer caching works
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-install-project --no-dev

# Now copy the app and install the project itself
COPY . .
RUN uv sync --frozen --no-dev

CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Two things worth calling out. --frozen means "fail if the lockfile is out of date" instead of silently re-resolving — exactly what you want in a build. And installing dependencies before copying your source means Docker caches that layer, so code changes don't trigger a full reinstall. Builds that used to take a couple of minutes now finish before I've switched windows.

The stuff nobody tells you (the genuinely interesting bits)

A few things I only discovered after switching, that I'd have loved to know earlier:

uv manages Python versions too. You don't need pyenv. Pin a version and uv will download and use it:

uv python pin 3.12

Dependency groups replace the requirements-dev.txt dance. Dev-only tools go in their own group and stay out of production:

uv add --dev ruff pytest mypy

Then uv sync --no-dev in your production image keeps the image lean automatically.

uvx runs tools without installing them into your project. Want to run Ruff once without polluting anything?

uvx ruff check .

It's like npx, but for Python, and it caches so the second run is instant.

The lockfile is the source of truth, and it's fast enough that you stop caring. Resolution that used to make me tab away to check my phone now happens in well under a second because uv resolves in Rust and caches aggressively across projects.

Was there anything annoying?

To be fair: pyproject.toml is a slightly different mental model than a flat requirements file, and if you have a deployment script or a colleague still typing pip install -r requirements.txt, you'll want to communicate the change. You can always export back with uv export --format requirements-txt > requirements.txt for anything that still expects the old format, which makes the transition painless. But once the whole pipeline is on uv, you won't reach for it.

Would I go back?

No. The honest summary is that uv took a stack of tools I'd internalized as "just how Python works" and replaced them with one fast binary and two commands I actually remember — uv add and uv sync. My FastAPI projects build faster, reproduce reliably, and I've stopped thinking about virtualenvs entirely.

If you're still on the pip + venv + requirements.txt treadmill, spend fifteen minutes migrating one project. That's all it took for me to become insufferable about it. Consider this me being insufferable at you — I wish I'd known sooner.

Written by Abhishek Ghimire

More from this topic