Why I Switched to UV and Ruff

Context: Notes on replacing a pyenv + venv + pip-tools + flake8/black/isort stack with UV and Ruff for day-to-day ML development.


UV: fast, reproducible environments

I started using UV because I was tired of watching pip install grind through dependency resolution for ten minutes every time I spun up a new environment with torch, transformers, and half a dozen other ML libraries. I’d also accumulated this messy stack of pyenv for Python versions, venv for environments, and pip-tools for locking — three tools doing what felt like one job.

Once I switched, uv add and uv sync just handle everything: creating the environment, resolving dependencies, and locking them in a uv.lock file, all in seconds instead of minutes. I don’t even manually activate the venv anymore — I just prefix commands with uv run and it uses the right environment automatically.

The part that actually sold me was reproducibility: when I hand off a project or come back to it three months later, uv sync rebuilds the exact same environment every time, which matters a lot when you’re trying to reproduce an experiment’s results.

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

# create a project with a locked environment
uv init my-project
cd my-project
uv add torch transformers numpy

# run something inside the env without manually activating it
uv run python train.py

# install a specific Python version
uv python install 3.12

# pin/replicate an environment elsewhere
uv sync

Ruff: linting and formatting without the friction

Ruff is very useful for keeping my code clean without the friction. I just run ruff check . --fix and ruff format ., and both finish almost instantly — fast enough that I run them on every save instead of just before a commit.

I also use it to catch dumb mistakes early: unused imports left over from a notebook-to-script conversion, inconsistent import ordering, deprecated syntax it can auto-upgrade. Since it’s a single tool with one config block in pyproject.toml, I’m not maintaining separate config files for four different linters anymore, which used to be its own small source of bugs.

# install
pip install ruff   # or: uv add --dev ruff

# lint
ruff check .

# lint + auto-fix
ruff check . --fix

# format (black-compatible)
ruff format .

Config lives right in pyproject.toml:

[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I", "UP"]  # errors, pyflakes, import sort, pyupgrade

Wiring them together

Most people wire both into pre-commit or CI:

- uv sync
- ruff check .
- ruff format --check .

Together, UV and Ruff have cut a chunk of the “environment and hygiene” overhead out of my day, so I spend more of it actually iterating on models instead of fighting tooling.




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Generative AI Tools | Generative AI | Vanderbilt University
  • AI-assisted coding — without violating your DUAs or leaking sensitive data