uv Python Tutorial: Fast Package and Project Management
uv replaces pip, venv, pip-tools, pipx, and pyenv with one fast tool. Learn the project workflow, the lockfile, inline script dependencies, and how to migrate.
A new teammate clones a repository. The README says to install the right Python with one tool, create a virtual environment with a second, install pinned requirements with a third, and install the linter globally with a fourth. Forty minutes later, something still fails to import. With uv, the same onboarding is two commands, and it finishes before the coffee does.
uv is a Python package and project manager written in Rust by Astral, the company behind Ruff. It began in February of this year as a faster drop-in for pip. Since version 0.3 in August, it also manages whole projects, command-line tools, and Python installations.
This guide uses uv 0.4.x and Python 3.12. You need a terminal and basic familiarity with dependencies. A virtual environment is a per-project folder of installed packages, and a lockfile is a file that records the exact version of every package, so that everyone installs the same set. If those ideas are new, start with venv and pip.
My position: uv is the first Python tool that makes the correct workflow, a locked and reproducible environment, also the easiest one. That matters more than its speed. Adopt it, and keep an exit path.
Install uv
# macOS and Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows PowerShell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# Any platform, if you already have pipx
pipx install uv
uv --version
uv 0.4.9
uv is a single binary. It does not need Python to be installed first, because it can download Python for you. Later, uv self update upgrades it in place when you used the standalone installer.
The uv Python project workflow
Create a project
uv init taskapi
cd taskapi
uv init writes a small, standard project:
taskapi/
.python-version the Python version this project uses
README.md
hello.py a sample script
pyproject.toml project metadata and dependencies
The pyproject.toml uses the standard [project] table, the same one that setuptools, Hatch, and Flit read. For background, see this guide to pyproject.toml and project layout.
Add dependencies
uv add httpx
Using Python 3.12.5
Creating virtualenv at: .venv
Resolved 8 packages in 214ms
Installed 7 packages in 12ms
+ anyio==4.4.0
+ certifi==2024.8.30
+ h11==0.14.0
+ httpcore==1.0.5
+ httpx==0.27.2
+ idna==3.8
+ sniffio==1.3.1
The timings are from my machine and will vary. That one command did four things. It created a virtual environment in .venv, resolved versions for the package and everything it needs, installed them, and recorded the result in two files.
Development tools go in a separate group, so that they are not installed in production:
uv add --dev pytest
Your pyproject.toml now looks like this:
[project]
name = "taskapi"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"httpx>=0.27.2",
]
[tool.uv]
dev-dependencies = [
"pytest>=8.3.3",
]
Next to it sits uv.lock. The two files have different jobs, and confusing them causes most early mistakes.
| File | Contains | Written by | Commit it? |
|---|---|---|---|
pyproject.toml |
What you asked for: direct dependencies with loose ranges | You, or uv add |
Yes |
uv.lock |
What you got: every package at an exact version, with hashes, for all platforms | uv only. Never edit it by hand | Yes |
.venv/ |
The installed files | uv | No |
The lockfile is universal. One uv.lock covers Linux, macOS, and Windows, so a teammate on a different operating system gets matching versions from the same file.
Run code without activating anything
uv run hello.py
uv run pytest
uv run python -c "import httpx; print(httpx.__version__)"
Hello from taskapi!
uv run executes a command inside the project’s environment. Before it does, it checks that the lockfile matches pyproject.toml and that the environment matches the lockfile, and it fixes either one if needed. You never run source .venv/bin/activate, and you never forget to install after pulling a colleague’s change.
The rest of the daily commands
| Command | What it does |
|---|---|
uv add PACKAGE |
Add a dependency, update the lockfile, and install |
uv remove PACKAGE |
Remove a dependency and whatever only it needed |
uv lock |
Recompute uv.lock from pyproject.toml |
uv lock --upgrade-package httpx |
Move one package to its newest allowed version |
uv sync |
Make .venv match the lockfile exactly, removing extras |
uv sync --locked |
The same, but fail if the lockfile is out of date. Use it in CI |
uv tree |
Show the dependency tree |
uv run COMMAND |
Run a command in the project environment |
A teammate’s whole setup is now git clone followed by uv run pytest. uv installs the right Python if it is missing, builds the environment from the lockfile, and runs the tests.
Manage Python versions
uv can download and manage Python interpreters, which replaces tools such as pyenv.
uv python install 3.12
uv python list
uv python pin 3.12
pin writes the version to .python-version, and every uv command in the project then uses it. If a project asks for a version you do not have, uv fetches it automatically. At the time of writing, Python 3.13 is a release candidate, with the final release due in October. You can try your project on it with uv run --python 3.13 pytest, without changing anything permanently.
One detail surprises people who are used to older environments. A uv environment contains no pip and no setuptools unless you add them. That is consistent with Python 3.12, which stopped pre-installing setuptools in new virtual environments. Use uv add and uv pip in place of pip.
Run tools with uvx
Command-line tools such as linters and formatters do not belong in your project’s dependencies. uvx runs a tool in a temporary, cached environment of its own.
uvx ruff check .
uvx pycowsay "hello"
uvx is short for uv tool run. The first run downloads the tool, and later runs start instantly from the cache. To put a tool on your PATH permanently, use uv tool install ruff. This replaces pipx. The Ruff linter comes from the same company, and the two tools are designed to work together.
Single-file scripts with their own dependencies
Many useful Python programs are one file, and sharing one has always been awkward. The recipient needs to know which packages to install. PEP 723 defines a standard comment block that lists a script’s dependencies inside the script. uv reads it.
Save this as status.py, anywhere, outside any project:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "httpx",
# ]
# ///
import sys
import httpx
def main() -> int:
url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
try:
response = httpx.get(url, timeout=10.0, follow_redirects=True)
except httpx.HTTPError as error:
print(f'request failed: {error}', file=sys.stderr)
return 1
print(f'{url} returned {response.status_code}')
return 0
if __name__ == '__main__':
raise SystemExit(main())
uv run status.py
https://example.com returned 200
uv reads the block, builds a cached environment with httpx, and runs the script. There is no project, no requirements file, and no manual install. You can add the block with a command, too: uv add --script status.py httpx.
Because PEP 723 is a standard, the file is not tied to uv. Other tools that implement the PEP can run it the same way. This is the feature I recommend trying first, since it is useful on its own and risks nothing.
The pip-compatible interface
If you are not ready to restructure a project, uv also works as a faster replacement for the commands you already use.
uv venv # create .venv
uv pip install -r requirements.txt # install into it
uv pip compile requirements.in -o requirements.txt # pin, like pip-tools
uv pip sync requirements.txt # make the environment match exactly
This mode changes nothing about your files. It reads and writes ordinary requirements.txt, so you can try it in CI for a week and revert with no cost. The limitation is that you do not get the project features: no uv.lock, no uv run auto-sync.
The myth: uv is just a faster pip
That description was accurate in February and is outdated now. Speed is the least important thing uv changes, although it is what people notice first.
On speed itself, the project claims installs 10 to 100 times faster than pip. Do not take that from a README or from me. Measure a cold and a warm install on a real project of yours:
rm -rf .venv
time uv sync
Run it twice. The second run uses uv’s global cache and links files into the environment instead of copying them. On my laptop, rebuilding a mid-sized environment from a warm cache takes well under a second.
Here is the consequence that matters, and it is the real insight of this tool. When an environment takes under a second to rebuild, it stops being something you maintain. It becomes a cache of the lockfile. You stop activating it, stop repairing it, and stop wondering what is installed in it. The lockfile is the truth, and .venv is a disposable copy. That is the workflow every Python guide has recommended for years, and uv is the first tool that makes it the path of least effort.
uv compared with the tools it replaces
| Job | Traditional tool | uv equivalent |
|---|---|---|
| Install packages | pip |
uv add, uv pip install |
| Create environments | venv, virtualenv |
Automatic, or uv venv |
| Pin dependencies | pip-tools |
uv lock, uv pip compile |
| Manage a project and its lockfile | Poetry, PDM | uv init, uv add, uv sync |
| Run command-line tools | pipx |
uvx, uv tool install |
| Install Python versions | pyenv |
uv python install |
The comparison with Poetry deserves a note. Both manage a project with a lockfile. uv stores dependencies in the standard [project] table, while Poetry uses its own [tool.poetry] table. uv is much faster and also manages Python itself. Poetry is older, stable, and past version 1.0.
Migrating an existing project
- From requirements.txt. Run
uv initin the project, thenuv add -r requirements.txt. Review the result, and move development tools into the dev group. - From a project that already has a
[project]table. Runuv lockanduv sync. uv reads the existing metadata. - From Poetry. Move dependencies from
[tool.poetry.dependencies]into[project]by hand, then runuv lock. - In CI. Install uv, then run
uv sync --lockedfollowed byuv run pytest. The--lockedflag fails the build if someone changedpyproject.tomlwithout updating the lockfile. - Compare the outcome. Run your full test suite, and diff the installed versions against the old environment before you delete it.
For deployment, the same commands build the environment inside your image or on your server, with one sentence of caution: pin the uv version there too, so a new release cannot change a production build.
How real teams use uv
- The lockfile is committed and enforced. CI runs
uv sync --locked, so a pull request cannot merge with a stale lock. - Nobody activates environments. Scripts, Makefiles, and documentation use
uv runfor every command. - The Python version lives in the repository.
.python-versionis committed, and uv installs that interpreter on any machine. - Tools run through uvx. Linters and formatters are invoked with pinned versions, and they stay out of the project’s dependencies.
- uv itself is pinned. Because the tool is young and changes quickly, teams fix its version in CI and upgrade deliberately.
In my experience moving a service with about ninety locked packages to uv last month, the CI install step dropped from over a minute to a few seconds with a warm cache. The larger change was quieter. Two long-standing “works on my machine” reports disappeared, because developers had been running against environments that no longer matched the requirements file. With uv run, the environment is checked on every command, so that drift cannot build up.
Choosing a workflow: a decision framework
- Is it a new project? Use
uv initand the project workflow. - Is it a single script with dependencies? Use PEP 723 inline metadata and
uv run. - Is it an existing project on pip and requirements files? Start with
uv pipas a drop-in, then move touv lockwhen you are comfortable. - Is it an existing Poetry project that works? There is no urgency. Migrate when you want faster installs or managed Python versions.
- Do you depend on conda packages or non-Python libraries? Stay with conda for those. uv installs from PyPI-style indexes.
When NOT to adopt uv yet
- Your organization requires stable, long-supported tooling. uv is below 1.0 and ships frequent releases, and commands and defaults have changed between versions. If you cannot absorb that, wait.
- Your platform builds from another tool’s lockfile. Some hosting services and security scanners understand
requirements.txtorpoetry.lock, and not yetuv.lock. Check before you switch. - You rely on conda for compiled scientific stacks. uv does not manage conda packages or system libraries.
One more consideration is governance. uv is developed by a single venture-backed company. The code is open source under permissive licenses, which limits the risk. The practical protection is the one mentioned earlier: keep dependencies in the standard [project] table. Then uv.lock is the only uv-specific file, and leaving means regenerating a lockfile with another tool.
Common mistakes
- Not committing uv.lock. Without it, every machine resolves versions on its own day and gets a different environment.
- Editing uv.lock by hand. The file is generated. Manual edits are overwritten or leave it inconsistent. Change
pyproject.toml, and runuv lock. - Running pip inside a uv environment.
pipis not installed there, and a globalpipinstalls somewhere else. Useuv addoruv pip. - Mixing activation with uv run. An activated environment from another project can confuse which packages you are using. Use
uv runconsistently. - Using uv sync without –locked in CI. A stale lockfile is silently updated during the build, so CI tests versions that nobody committed.
- Leaving uv unpinned in automation. A new release changes behavior, and builds that passed yesterday fail today.
Key takeaways
- uv combines the roles of pip, venv, pip-tools, pipx, and pyenv in one binary.
uv init,uv add, anduv runcover the daily project workflow.- Commit
pyproject.tomlanduv.lock, and never commit.venv. uv runsyncs the environment before every command, so you never activate it.- PEP 723 inline metadata lets one script carry its own dependencies.
uv pipis a low-risk way to try uv on an existing project.- uv is young and pre-1.0, so pin its version and keep metadata in standard tables.
FAQ
What is uv in Python?
uv is a fast Python package and project manager written in Rust. It installs packages, creates virtual environments, locks dependencies, runs scripts and tools, and installs Python versions.
Is uv a replacement for pip?
Yes, and more. uv pip install is a drop-in for pip, and the project commands uv add, uv lock, and uv sync also replace venv, pip-tools, and much of Poetry’s role.
How is uv different from Poetry?
Both manage projects with a lockfile. uv is faster, stores dependencies in the standard [project] table, and also installs Python versions and runs tools. Poetry is older and more established.
Do I need to activate a virtual environment with uv?
No. uv run executes commands inside the project’s environment and keeps it in sync with the lockfile automatically.
What is uvx?
uvx is a shortcut for uv tool run. It runs a command-line tool, such as a linter, in an isolated cached environment without adding it to your project.
Lock everything, activate nothing
uv turns a set of Python habits that took discipline into defaults. The environment follows the lockfile, the lockfile follows pyproject.toml, and one command keeps them aligned. Start with a script or a new project, keep your metadata standard, and let the speed change how often you rebuild.
Rule of thumb: treat the lockfile as the source of truth and the environment as a cache you can delete at any time.
