Python uv: Fast Package and Project Management

uv is a fast, all-in-one tool for managing Python projects. It creates virtual environments, installs packages, keeps a lock file of exact versions, installs Python itself, and runs command-line tools. It’s made by Astral, the company behind the Ruff linter, and it’s written in Rust, which is a big part of why it’s so fast.

This article assumes you know what a virtual environment is and how to install packages with pip. uv does the same jobs, so everything you learned there still applies. You’ll learn:

  • What uv offers over plain pip and venv, and over Poetry, with measured numbers
  • How to install uv
  • How to use uv as a faster replacement for venv and pip
  • How to start a project with uv init, add packages with uv add, and run code with uv run
  • What the uv.lock file does and how uv sync uses it
  • How to install and pick Python versions with uv
  • How to install and run command-line tools with uv tool and uvx
  • How to move an existing project from requirements.txt or Poetry to uv

Why use uv?

Until recently, a typical Python setup needed several tools: venv for environments, pip for packages, pipx for command-line tools, pyenv for Python versions, and something like Poetry to lock dependency versions. uv covers all of these with one command.

It doesn’t replace the ideas, though. A uv project still has a virtual environment in a .venv directory, and the packages still come from PyPI. That’s why I recommend learning venv and pip first. Once you know what uv does for you, it stops feeling like magic.

What uv offers over pip and venv

The first thing everybody notices is speed. I timed both on my laptop (an Apple M2 Max, Python 3.15.2, pip 26.2.2, uv 0.13.7), installing five common packages into a fresh virtual environment: requests, rich, flask, pandas, and matplotlib. With their dependencies, that’s 29 packages. Each number is the middle of three runs:

Stepvenv and pipuv
Create a virtual environment1.4 s0.08 s
Install the packages, empty cache12.7 s3.2 s
Install them again in a new venv, cache filled8.0 s0.10 s

The first install is mostly downloading, so your internet connection decides a lot of it. The second one shows the real difference. pip still unpacks and copies every file, while uv puts the packages in place straight from its cache: it clones the files on macOS and Linux, which costs almost no time or extra disk space, and hard links them on Windows. That cache takes room of its own, though. After this test, uv’s cache was 141 MB and pip’s was 40 MB, because uv stores packages unpacked.

Speed is nice, but these differences matter more in daily use:

  • One tool for everything: virtual environments, packages, Python versions, and command-line tools. You don’t have to install and learn pipx and pyenv separately.
  • A real lock file. pip freeze gives you a snapshot of one environment that you have to remember to update. uv keeps uv.lock up to date on every change, with exact versions and file hashes for every operating system at once, and uv sync rebuilds exactly that environment anywhere.
  • No activation needed. uv run checks that .venv matches your project, fixes it if needed, and runs your code in it. The “I installed it but Python can’t find it” problem from the venv article mostly disappears.
  • Clear errors when versions conflict. Both pip and uv refuse impossible combinations, but uv explains its reasoning step by step. More on that below.

Here’s an example of that last point. I asked for a version of urllib3 that is too old for the current requests:

$ uv pip install "requests==2.34.2" "urllib3<1.21"
error: No solution found when resolving dependencies
  cause: Because requests>=2.34.2 depends on urllib3>=1.26,<3 and you require requests==2.34.2, we can conclude that you require urllib3>=1.26,<3.
         And because you require urllib3<1.21, we can conclude that your requirements are unsatisfiable.Code language: Shell Session (shell)

pip refuses this too, and its message names the same conflict, but only after fetching urllib3’s metadata and printing that it’s “looking at multiple versions of requests”, which can take a while in bigger projects.

What uv offers over Poetry

Poetry already gave Python a lock file and a single tool for projects years before uv existed, so the gap is smaller here. I repeated the test with Poetry 2.5.1: a new project, adding the same five packages, and then rebuilding the environment from the lock file. Again the middle of three runs:

StepPoetryuv
Add the packages: resolve, lock, and install (empty cache)6.3 s (poetry add)3.4 s (uv add)
Rebuild .venv from the lock file (cache filled)1.95 s (poetry install)0.11 s (uv sync)

Apart from speed:

  • uv installs Python versions for you, and downloads a missing one automatically. Poetry 2.5 has a poetry python install command too, but it’s still marked experimental, so most Poetry users still need pyenv or another installer.
  • uv replaces pipx as well, with uv tool install and uvx. Poetry only manages projects.
  • uv has a pip-compatible interface. You can start with uv venv and uv pip install in a project that uses requirements.txt, and move to uv add later, or never. Poetry expects to manage the whole project from the start.
  • uv is a single program that doesn’t depend on any Python installation. Poetry is itself a Python package, so it needs a working Python and its own isolated install, usually with pipx.

One old difference is gone: Poetry used to keep your dependencies in its own [tool.poetry] section. Since Poetry 2.0, new projects use the standard [project] table in pyproject.toml, just like uv. That makes switching in either direction easier.

pip and venv, Poetry, and uv compared

pip and venvPoetryuv
Comes with PythonYesNoNo
Creates virtual environmentsYesYesYes
Lock file with exact versionsNo, only pip freezeYes, poetry.lockYes, uv.lock
Dependencies in standard [project] tableIf you write it yourselfYes, since 2.0Yes
Installs Python versionsNoExperimentalYes
Installs command-line toolsNo, use pipxNo, use pipxYes, uv tool and uvx
Works without activating the venvNoYes, poetry runYes, uv run
SpeedSlowestIn betweenFastest

When pip and venv or Poetry are still fine

uv is my choice for new projects, but there are good reasons to stay with the others:

  • You’re learning. venv and pip come with Python and show you every step, which is exactly why this tutorial teaches the venv and pip pages first.
  • You can’t install extra tools, for example on a locked-down work computer or a server you don’t manage. Python with venv and pip is usually there already.
  • Your project already uses Poetry and it works. Its lock file and workflow do the job; switching is only worth it if the speed or Python management matters to you. The same goes for publishing: if you already build and publish packages with poetry build and poetry publish, there’s no need to change that.
  • A small script with one or two dependencies hardly needs a project manager at all.

Keep in mind that uv is open source, but developed by a company, Astral. Poetry and pip are community projects. For most learners that changes nothing, but it’s worth knowing what you’re depending on.

Install uv

uv is a single program that doesn’t depend on Python, so you install it separately. The official installer works on macOS and Linux:

curl -LsSf https://astral.sh/uv/install.sh | shCode language: Shell Session (shell)

On Windows, run this in PowerShell:

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"Code language: Shell Session (shell)

You can also use a package manager: brew install uv with Homebrew, winget install --id=astral-sh.uv -e on Windows, or pipx install uv. Open a new terminal afterwards and check that it works:

$ uv --version
uv 0.13.7Code language: Shell Session (shell)

If you used the official installer, uv self update updates uv later. With a package manager, update it the way you installed it, for example brew upgrade uv.

uv as a replacement for venv and pip

The easiest way to start is to use uv exactly like the tools you already know. Put uv in front of the pip commands, and use uv venv to create the environment:

$ uv venv
Using CPython 3.15.2 interpreter at: /opt/homebrew/opt/python@3.15/bin/python3.15
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate
$ uv pip install requests
 ...
 + requests==2.34.2
 + urllib3==2.8.0
$ uv pip list
Package            Version
------------------ ----------
certifi            2026.10.23
charset-normalizer 3.5.2
idna               3.20
requests           2.34.2
urllib3            2.8.0Code language: Shell Session (shell)

Two things are different from pip. uv venv uses .venv as the directory name unless you give it another one. And uv pip finds the .venv in the current directory by itself, so you don’t have to activate it before installing. You still activate it, the same way as any venv, when you want to run python in it.

Here’s how the commands from the venv and pip articles map to uv:

Jobvenv and pipuv
Create a venvpython -m venv .venvuv venv
Install a packagepython -m pip install requestsuv pip install requests
Install from a filepython -m pip install -r requirements.txtuv pip install -r requirements.txt
Write down what’s installedpython -m pip freeze > requirements.txtuv pip freeze > requirements.txt
List packagespython -m pip listuv pip list
Remove a packagepython -m pip uninstall requestsuv pip uninstall requests

If you want to practice this mapping step by step, the free lesson Meet uv after the pieces make sense lets you do it in a terminal in your browser, with nothing to install.

This alone makes installs a lot faster. But uv’s real strength is managing a whole project for you, so let’s look at that next.

Start a project with uv init

In project mode, you don’t run pip commands yourself. You tell uv which packages your project needs, and uv keeps the virtual environment in line with that list. Create a new project with uv init:

$ uv init myproject
Initialized project `myproject` at `/home/erik/projects/myproject`
$ cd myprojectCode language: Shell Session (shell)

uv created these files:

myproject ├── .git ├── .gitignore ├── .python-version ├── README.md ├── pyproject.toml └── src └── myproject └── __init__.py

It also started a git repository, and its .gitignore already excludes the .venv directory. The .python-version file records which Python version the project uses. The most important file is pyproject.toml, the standard file that describes a Python project:

[project] name = “myproject” version = “0.1.0” description = “Add your description here” readme = “README.md” requires-python = “>=3.15” dependencies = [] [project.scripts] myproject = “myproject:main” [build-system] requires = [“uv_build>=0.13.7,<0.14.0"] build-backend = "uv_build"

uv also fills in an authors line from your git settings, which I left out here. The dependencies list is empty for now. requires-python comes from the Python version uv found on your system; we’ll change it further on.

By default, uv sets the project up as a package in a src directory, with a main() function in src/myproject/__init__.py that you can run as a command. If you’d rather have a single main.py file and no package, use uv init --no-package myproject instead.

Run code with uv run

To run something inside the project, put uv run in front of it. The [project.scripts] section above created a myproject command, so let’s run that:

$ uv run myproject
Using CPython 3.15.2 interpreter at: /opt/homebrew/opt/python@3.15/bin/python3.15
Creating virtual environment at: .venv
   ...
Hello from myproject!Code language: Shell Session (shell)

Before running anything, uv created the .venv directory and a uv.lock file, and installed the project into the venv. Every uv run checks first that the environment matches pyproject.toml and fixes it if it doesn’t. That’s why you don’t need to activate the venv in a uv project. uv run works with anything you’d normally type after activating:

$ uv run python --version
Python 3.15.2
$ uv run python my_script.pyCode language: Shell Session (shell)

You can still activate .venv if you prefer, or point your editor at .venv. VS Code finds it automatically.

Add and remove packages with uv add and uv remove

To add a package to the project, use uv add:

$ uv add requests
Resolved 6 packages in 127ms
   ...
 + certifi==2026.10.23
 + charset-normalizer==3.5.2
 + idna==3.20
 + requests==2.34.2
 + urllib3==2.8.0Code language: Shell Session (shell)

uv did three things at once: it installed requests and its dependencies in .venv, it added requests to pyproject.toml, and it recorded the exact versions in uv.lock. The dependencies list in pyproject.toml now reads:

dependencies = [ “requests>=2.34.2”, ]

uv writes a minimum version: the version it just installed, or newer. If you want something else, give it the same version specifiers as pip, like uv add "requests>=2.32,<3". Now try the package in a script. Save this as latest.py in the project directory:

import requests

response = requests.get("https://pypi.org/pypi/requests/json")
print(response.json()["info"]["version"])Code language: Python (python)
$ uv run latest.py
2.34.2Code language: Shell Session (shell)

It asks PyPI for the latest version of requests and prints it. Some packages you only need while developing, like a test framework. Add those as development dependencies with --dev. They go in a separate [dependency-groups] section of pyproject.toml:

$ uv add --dev pytestCode language: Shell Session (shell)

To remove a package, use uv remove. Unlike pip uninstall, it also removes the dependencies that nothing else needs anymore:

$ uv remove requests
Resolved 7 packages in 5ms
   ...
 - certifi==2026.10.23
 - charset-normalizer==3.5.2
 - idna==3.20
 - requests==2.34.2
 - urllib3==2.8.0Code language: Shell Session (shell)

To see which package pulled in which, use uv tree:

$ uv tree myproject v0.1.0 ├── requests v2.34.2 │ ├── certifi v2026.10.23 │ ├── charset-normalizer v3.5.2 │ ├── idna v3.20 │ └── urllib3 v2.8.0 └── pytest v9.2.0 (group: dev) ├── iniconfig v2.3.1 ├── packaging v26.3 ├── pluggy v1.7.0 └── pygments v2.21.0

The lock file and uv sync

pyproject.toml says what your project needs, like “requests 2.34.2 or newer”. The uv.lock file records exactly what was installed: every package, including the dependencies of your dependencies, with its exact version and a hash of the file. It’s what pip freeze > requirements.txt does for you on the pip page, except that uv keeps it up to date automatically and you never edit it by hand.

Commit both pyproject.toml and uv.lock to git, and leave .venv out. Anyone who clones the project, including you on another computer, then gets exactly the same versions with one command:

$ uv sync
Using CPython 3.15.2 interpreter at: /opt/homebrew/opt/python@3.15/bin/python3.15
Creating virtual environment at: .venv
Resolved 12 packages in 4ms
Installed 11 packages in 11ms
 + certifi==2026.10.23
 ...Code language: Shell Session (shell)

uv sync creates the .venv if it’s missing and makes it match the lock file exactly. That also makes a broken venv easy to fix: delete .venv and run uv sync, or simply uv run, which syncs first.

Because the lock file pins exact versions, uv won’t upgrade anything until you ask. To move one package to the newest version that pyproject.toml allows, use uv lock --upgrade-package requests, then uv sync. uv lock --upgrade does this for all packages.

Install and pick Python versions

uv can download Python for you, so you don’t need a separate tool like pyenv. Install a version with uv python install:

$ uv python install 3.14
Installed Python 3.14.10 in 771ms
 + cpython-3.14.10-macos-aarch64-none (python3.14)Code language: Shell Session (shell)

The (python3.14) at the end means uv also made a python3.14 command available, in ~/.local/bin on macOS and Linux. uv python list shows every Python uv can find on your computer, including the ones you installed in other ways.

You usually don’t even need that step. When a project asks for a Python version you don’t have, uv run and uv sync download it automatically. To choose the version when you create a project, pass --python:

$ uv init --python 3.14 myprojectCode language: Shell Session (shell)

This sets requires-python = ">=3.14" in pyproject.toml and writes 3.14 to .python-version. To switch an existing project to another version, use uv python pin:

$ uv python pin 3.15
Updated `.python-version` from `3.14` -> `3.15`
$ uv run python --version
Python 3.15.2Code language: Shell Session (shell)

The pinned version has to fit requires-python. In a project that requires Python 3.15 or newer, uv python pin 3.14 fails with an error saying the version is incompatible with the project’s requires-python value. To go back to an older Python, lower requires-python in pyproject.toml first.

Command-line tools: uv tool and uvx

Some Python packages are programs you want to use everywhere, not libraries for one project: a code formatter, a linter, Jupyter. On the pip page, we used pipx for those. uv has the same feature built in:

$ uv tool install ruff
 + ruff==0.17.4
Installed 1 executable: ruff
$ uv tool list
ruff v0.17.4
- ruffCode language: Shell Session (shell)

Like pipx, uv gives every tool its own hidden virtual environment, so tools never get in each other’s way or in the way of your projects. If uv warns that the tool directory is not on your PATH, run uv tool update-shell once and open a new terminal. uv tool upgrade ruff upgrades a tool, and uv tool uninstall ruff removes it.

If you only need a tool once, you don’t have to install it at all. uvx downloads it into a temporary environment and runs it:

$ uvx ruff --version
ruff 0.17.4
$ uvx ruff check .Code language: Shell Session (shell)

The first command prints the version; the second lets ruff check all the Python files in the current directory. The second time you use a tool this way, it runs straight from uv’s cache. uvx is short for uv tool run.

Migrating to uv

Before you migrate anything, commit your work to git, or make a copy of the project. That way, you can always compare with the old version or go back.

From requirements.txt

In the project directory, create a pyproject.toml and import your requirements files:

$ uv init --bare
$ uv add -r requirements.txt
$ uv add --dev -r requirements-dev.txtCode language: Shell Session (shell)

--bare creates only pyproject.toml and leaves your existing files alone. uv add -r copies each line of the file, version specifier and all, into the dependencies list and installs everything. The third line is only needed if you keep development tools like pytest in a separate file. From then on, use uv add and uv sync, and you can delete the requirements files.

If you don’t want to switch yet, remember that uv also works as a drop-in replacement: uv venv followed by uv pip install -r requirements.txt gives you the speed without changing anything in the project.

From Poetry

For Poetry projects, there’s a community tool called migrate-to-uv. You don’t need to install it; run it with uvx in the project directory:

$ uvx migrate-to-uv
Locking dependencies with constraints from existing lock file(s) using "uv lock"...
Using CPython 3.15.2 interpreter at: /opt/homebrew/opt/python@3.15/bin/python3.15
Resolved 15 packages in 98ms
Successfully migrated project from Poetry to uv!Code language: Shell Session (shell)

It rewrites pyproject.toml to the standard format, turns Poetry’s version constraints like ^2.32 into regular ones like >=2.32,<3, moves Poetry’s dependency groups to [dependency-groups], and replaces poetry.lock with a uv.lock that keeps the versions you had locked. I tried it (version 0.12.0) on an old-style Poetry project and on one made with Poetry 2.5; both worked. Add --dry-run to see the new pyproject.toml without changing anything.

Read the warnings at the end. If your project is a package you publish, migrate-to-uv may switch the build backend, for example to Hatch. Check that uv build produces the same files as poetry build did before you publish a release.

Back to requirements.txt

It works the other way around too. If someone needs a requirements.txt file, for example a hosting service that only understands pip, uv export writes one from the lock file:

$ uv export --no-dev --no-hashes -o requirements.txtCode language: Shell Session (shell)

uv cheat sheet

CommandWhat it does
uv init myprojectCreate a new project
uv add requestsAdd a package to the project and install it
uv add --dev pytestAdd a development-only package
uv remove requestsRemove a package from the project
uv run script.pyRun a script in the project’s environment
uv syncMake .venv match the lock file
uv lock --upgradeUpgrade all packages within the allowed versions
uv treeShow which package needs which
uv python install 3.14Install a Python version
uv python pin 3.14Set the project’s Python version
uv venv / uv pip installUse uv like venv and pip
uv tool install ruffInstall a command-line tool
uvx ruff check .Run a tool without installing it
uvx migrate-to-uvConvert a Poetry project to uv

Continue learning

The free lesson Meet uv after the pieces make sense is part of our Python in Practice course. That course starts with venv and pip and gets to uv once the pieces make sense, like this tutorial does.

This article is part of a free Python Tutorial. You can browse the tutorial with the navigation buttons at the top and bottom of the article or use the navigation menu. Here are some related reads:

Related articles