If you started a Python project in 2020, your toolbelt looked like this: pyenv to install Python versions, venv to make virtual environments, pip to install packages, pip-tools (or Poetry) to lock them, and setuptools to build wheels. Five tools, five config files, five places where things could go wrong.
In 2026, more and more people just use uv.
What uv is
uv is a Python package and project manager from Astral — the same company behind Ruff. It launched in early 2024 as a faster pip replacement, then over the next year quietly grew until it could replace the whole stack:
- virtual environment creation (replaces
venv,virtualenv) - package installation (replaces
pip) - dependency resolution and locking (replaces
pip-tools, parts of Poetry) - project initialization and management (replaces parts of Poetry, PDM, Hatch)
- Python interpreter management (replaces
pyenv) - build backend integration (works with hatchling, setuptools, etc.)
It’s written in Rust, distributed as a single static binary, and — this is not marketing fluff — genuinely 10× to 100× faster than pip for most operations. The “install a hundred packages in two seconds” demos are real. By mid-2025 it had become the default recommendation in most Python tutorials, and as of 2026 it’s what I’d reach for on a new project.
Installing it
You don’t pip install uv (well, you can, but you shouldn’t — uv is a tool, not a Python package). Install it as a system binary:
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
# or via your package manager
brew install uv
That’s it. No Python required to install uv itself, which is nice because uv can then go install Python for you.
The “I just want a project” workflow
This is the path you’ll take 90% of the time:
uv init weather-cli
cd weather-cli
That gives you a populated directory:
weather-cli/
├── .python-version
├── .gitignore
├── README.md
├── main.py
└── pyproject.toml
The pyproject.toml is already filled in with sensible defaults. Add a dependency:
uv add httpx
Three things happen instantly:
httpx(and its dependencies) are added to[project.dependencies]inpyproject.toml.- A
.venv/is created if it didn’t exist, and the packages are installed into it. - A
uv.lockfile is generated, pinning every package and transitive dependency to an exact version with hashes.
Run your code:
uv run python main.py
uv run makes sure the venv is in sync with the lockfile before running, so you can never end up in the “did I forget to install something?” state. If pyproject.toml says you need a package that isn’t installed, uv installs it. If the lockfile is out of date, uv updates it. You don’t activate anything. You don’t think about anything.
To remove a package: uv remove httpx. To update everything: uv lock --upgrade then uv sync. To install a specific package version: uv add 'httpx>=0.27,<0.30'.
The lockfile
uv.lock is the equivalent of poetry.lock or pip-tools’ compiled requirements.txt. It records the exact resolved version of every package, every transitive dependency, every URL, and every hash. Commit it to git. This is what makes builds reproducible — anyone who clones your repo and runs uv sync will get byte-identical packages to what you have.
pyproject.toml says what you want (httpx>=0.27). uv.lock says what you got (httpx==0.27.2, plus the 14 transitive packages it pulled in). When you deploy, you install from the lock, not from the loose declarations. That’s the only way to avoid “but it worked in dev.”
The pip-compatible mode
If you have an existing project and don’t want to switch your workflow, uv has a drop-in pip replacement:
uv pip install requests
uv pip install -r requirements.txt
uv pip freeze > requirements.txt
uv venv # create a venv (replaces python -m venv)
The flags and behaviour mirror pip’s, just much faster. This was uv’s original mode when it launched, and it’s still useful for legacy projects, CI scripts, and anything that already speaks “pip.” But for new projects, prefer the project mode (uv add, uv run) — it’s where uv actually shines.
Python interpreter management
This is the bit that surprised me the first time I used it. uv can install Python itself.
uv python install 3.13
uv python list
These are real Python builds (from the python-build-standalone project), installed into uv’s cache. When you create a project, uv reads requires-python from your pyproject.toml and either uses an installed interpreter that matches or downloads one automatically.
# pyproject.toml
[project]
requires-python = ">=3.13"
If 3.13 isn’t installed, uv sync fetches it. The pyenv step of the old workflow just disappears. (You can still use system Python or pyenv-installed Pythons if you want — uv looks at all of them.)
There’s also a .python-version file (created by uv init) that pins the project to a specific version, so every collaborator gets the same interpreter without coordination.
Migrating from other tools
From pip + venv + requirements.txt:
cd existing-project
uv init --no-readme --no-pin-python # creates pyproject.toml, doesn't overwrite
uv add -r requirements.txt # imports your existing pins
You now have a pyproject.toml and a uv.lock. Delete requirements.txt once you’re confident.
From Poetry: uv reads Poetry-style pyproject.toml partially, but the cleanest path is uvx migrate-to-uv (a one-shot conversion tool). It rewrites the [tool.poetry] sections into PEP 621 [project] form and generates a uv.lock from your poetry.lock.
From conda: uv doesn’t replace conda for the scientific stack with C/Fortran dependencies — that’s still conda’s strength. But for pure-Python projects, you can usually swap conda for uv and get faster, lighter environments. For mixed projects, pixi (also Rust, also fast) is the conda-shaped equivalent.
A new-project walkthrough
Let’s start one from scratch, end to end. Imagine a small ETL script that pulls JSON from an API and writes Parquet.
uv init etl-job --python 3.13
cd etl-job
Add the dependencies:
uv add httpx polars
uv add --dev pytest ruff mypy
--dev adds them to a dev dependency group (PEP 735) instead of the main dependencies. They get installed in your local environment but don’t ship with the package.
Look at pyproject.toml:
[project]
name = "etl-job"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
"httpx>=0.27.2",
"polars>=1.5.0",
]
[dependency-groups]
dev = [
"pytest>=8.3",
"ruff>=0.5",
"mypy>=1.11",
]
Write a test:
# tests/test_etl.py
def test_arithmetic():
assert 2 + 2 == 4
Run it:
uv run pytest
uv ensures pytest and its deps are installed (they are — they’re in the dev group, which is installed by default), then runs them in the project venv. Same pattern for everything:
uv run ruff check .
uv run mypy src/
uv run python -m etl_job
Need a one-off tool that isn’t part of your project? Use uvx (alias for uv tool run):
uvx black . # run black without installing it persistently
uvx --from cookiecutter cookiecutter gh:audreyr/cookiecutter-pypackage
uvx runs ephemeral, isolated tools — like pipx run but instantaneous because uv caches everything. I use it dozens of times a day for things like uvx ruff format, uvx httpie, uvx ipython.
Why it’s so fast
A few reasons matter:
- Parallel downloads. pip downloads packages serially; uv downloads in parallel and starts extracting wheels as soon as bytes arrive.
- Smarter resolver. uv uses PubGrub, the same algorithm Cargo and Dart’s pub use. It backtracks intelligently and gives much better error messages when a resolution fails.
- Global cache with hard links. uv keeps one copy of each package version on disk and hard-links into project venvs. Creating a fresh venv with 50 packages takes hundreds of milliseconds because nothing is actually copied.
- No per-call Python startup. pip is a Python program, so every invocation pays Python’s startup tax. uv is a native binary; it starts in a few milliseconds.
You feel this most on cold installs and CI. A typical pip install -r requirements.txt for a 50-dependency project might take 30-60 seconds. The same with uv sync is often under two seconds.
CI with uv
The CI story is where the speed difference compounds. A typical GitHub Actions job:
- uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- run: uv sync --frozen
- run: uv run pytest
- run: uv run ruff check .
--frozen tells uv to refuse to update the lockfile — if pyproject.toml and uv.lock are out of sync, the build fails instead of silently re-resolving. That’s what you want in CI: builds either match the committed lockfile exactly or they don’t run. The enable-cache: true keeps uv’s package cache between runs, so your second push installs in milliseconds.
This is also where uv’s hard-link trick pays off — even on a cold cache, downloading and resolving 50 packages on a CI runner usually finishes before the action’s first log line scrolls past.
What it doesn’t do
- It’s not a build backend. Use hatchling (or your favourite). uv calls into the backend via PEP 517.
- It’s not a publish tool… well, mostly.
uv publishexists in 2026 and uploads to PyPI, but the workflow is still “build the wheel, then upload it.” - It doesn’t manage non-Python dependencies. If you need libpq or ffmpeg installed system-wide, uv won’t help. (For that, look at pixi or your OS package manager.)
- It doesn’t replace your IDE’s Python detection magic. VS Code, PyCharm, etc. need to be pointed at
.venv/(or its absolute path) like with any other tool. Most editors auto-detect a.venvdirectory at the project root, which is exactly whatuvcreates.
Things to know that aren’t in the docs
A few details that bit me before I learned them:
uv syncdeletes packages that aren’t in the lockfile. This is the right behaviour — it keeps the venv consistent with what’s tracked — but if youuv pip install something-experimentalto try it out, the nextuv syncwill yank it. To keep it, runuv addinstead.uv runalways re-syncs first unless you pass--no-sync. If you have a long pre-flight check or a slow lockfile resolution, that adds latency. For a tight inner loop (e.g. running a single test repeatedly),--no-syncis your friend.- Tool versions are independent.
uvx ruffruns the latest ruff.uv run ruffruns the version pinned in your project’s dev dependencies. They’re different commands and they can disagree. For project-related lint, preferuv runso everyone gets the same version.
The takeaway
Five tools collapsed into one. The workflow is uv init, uv add, uv run, uv sync. The mental overhead is gone. If you’re starting a new Python project in 2026, this is the path of least resistance — and the one most of the ecosystem has converged on.
Now that you can install things and lock them, the next lesson is about where to put them: how a Python project should be laid out, src/ versus flat, where tests and scripts live, and the conventions that have settled by 2026.