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: