Managing Python environments doesn't have to be hard. A lot of the manual and error prone processes can be handled automatically with the right tool.
This tutorial introduces uv. An extremely fast and popular Python package and project manager. A single tool to replace pip
, pip-tools
, pipx
, poetry
, pyenv
, twine
, virtualenv
, and more. It can even install and manage Python versions.
With homebrew:
$ brew install uv
Click here for other installation methods.
To verify the installation, run uv
and you should see a help menu listing available commands.
Note
Use homebrew to upgrade or uninstall uv.
Python does not need to be explicitly installed to use uv. By default, uv will automatically download Python versions when they are required. Even if a specific Python version is not requested, uv will download the latest version on demand.
For example, if there are no Python versions on your system, the following will install Python before creating a new virtual environment:
$ uv venv
If Python is already installed on your system, uv will detect and use it without configuration. You can also install and manage specific Python versions.
# Install the latest version
$ uv python install
# Install a specific version
$ uv python install 3.12
# Install multiple versions
$ uv python install 3.11 3.12
# Install an alternative implementation
$ uv python install pypy@3.11
Note
Python does not publish official distributable binaries. As such, uv uses distributions from the Astral python-build-standalone project. See the Python distributions documentation for more details.
To reinstall uv-managed Python versions, use --reinstall
:
$ uv python install --reinstall
This will reinstall all previously installed Python versions. Improvements are constantly being added to the Python distributions, so reinstalling may resolve bugs even if the Python version does not change.
To uninstall uv-managed Python versions:
$ uv python uninstall 3.11
To view available and installed Python versions:
$ uv python list
A specific Python version can be requested with the --python
flag in most uv commands. For example, when creating a virtual environment:
$ uv venv --python 3.13.3
If you have a specific Python version you'd like to use as the default, the .python-version
file is helpful. It can be created with the uv python pin
command:
# Created in the user configuration directory
$ uv python pin --global 3.13.3
# Created in the current working directory
$ uv python pin pypy@3.11
Before we talk about Python projects, let's quickly go over scripts. A Python script is a file intended for standalone execution (e.g. python <script>.py
).
If your script has no dependencies, or depends on modules in the standard library, you can execute it with uv run
with nothing more to think about:
# example.py
import os
print(os.path.expanduser("~"))
$ uv run example.py
/Users/python-enjoyer
Note that if you use uv run
in a project (a directory with a pyproject.toml
), it will install the current project before running the script. If your script does not depend on the project, use the --no-project
flag to skip this:
$ uv run --no-project example.py
Note
The --no-project
flag is part of uv and not the script, so it must be specified before the script name.
If your script has dependencies it's recommended to create a project or use Python's new format for inline metadata. Inline metadata allows dependencies for a script to be declared in the script itself.
# Initialize a script with inline metadata
$ uv init --script example.py --python 3.12
# Add dependencies to a script
$ uv add --script example.py 'requests<3' 'rich'
# example.py
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "requests<3",
# "rich",
# ]
# ///
import requests
from rich.pretty import pprint
resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])
Important
Scripts that declare inline metadata are automatically executed in environments isolated from the project. Only the dependencies listed in the script are available. The --no-project
flag is not required.
A shebang can be added to make a script executable without using uv run
:
#!/usr/bin/env -S uv run --script
print("Hello, world!")
Ensure the script is executable (chmod +x greet
) then run the script:
$ ./greet
Hello, world!
You can initialize a brand new project:
$ uv init example
$ cd example
Or, initialize an existing project:
$ cd example
$ uv init
This will create the following files:
.
├── .python-version
├── README.md
├── main.py
└── pyproject.toml
The main.py
file contains a simple "Hello world" program. Try it with uv run
:
$ uv run main.py
Hello from example!
You'll notice the project structure will now look like this:
.
├── .python-version
├── .venv
├── README.md
├── main.py
├── pyproject.toml
└── uv.lock
Except for .venv
, all of this should be checked into version control.
The project's default Python version. This file tells uv which Python version to use when creating the project's virtual environment.
The .venv folder contains your project's virtual environment, a Python environment that is isolated from the rest of your system. This is where uv will install your project's dependencies.
Notice how uv created this for you automatically. Very nice!
The project's README file.
Contains a simple "Hello world" program.
Contains metadata about your project. Use this file to specify dependencies, as well as details about the project such as its description or license. You can edit this file manually, or use commands like uv add
and uv remove
.
You'll also use this file to specify uv configuration options in a [tool.uv]
section.
A cross-platform lockfile that contains exact information about your project's dependencies. It's a human-readable TOML file, but it's managed by uv and shouldn't be edited manually.
Add or remove dependencies with uv add
and uv remove
. This will update pyproject.toml
, uv.lock
, and the project environment.
# Add a dependency
$ uv add requests
# Specify a version constraint
$ uv add 'requests==2.30.0'
# To update a version constraint
$ uv add 'requests==2.31.0'
# Add a git dependency (--tag, --branch, or --rev can be used for version constraint)
$ uv add git+https://github.com/psf/requests
# Migrating from requirements.txt
$ uv add -r requirements.txt -c constraints.txt
# Remove a dependency
$ uv remove requests
To see the dependency tree for your project:
$ uv tree
Use uv lock
to upgrade a package:
# Upgrade to the latest version
$ uv lock --upgrade-package requests
# Upgrade to a specific version
$ uv lock --upgrade-package 'requests==2.32.3'
# Upgrade all packages:
$ uv lock --upgrade
Version constraints are respected when upgrading.
Tip
Instead of upgrading a package, update its version constraint.
Caution
It's important to understand how we end up here.
When considering if the lockfile is up-to-date, uv will check if it matches the project metadata. For example, if you add a dependency to your pyproject.toml
, the lockfile will be considered outdated. Similarly, if you change the version constraints for a dependency such that the locked version is excluded by the constraint, the lockfile will be considered outdated. However, if you change the version constraints such that the existing locked version is still within the constraint, the lockfile will still be considered up-to-date.
The last case is important.
This means, if you have requests>=2.32.3
as a version constraint, uv will not consider the lockfile outdated when a new version is released. The lockfile needs to be explicitly updated if you want to upgrade the dependency.
If you have lots of developers and systems, you could easily be in a situation where some machines are running 2.32.3
and some machines are running a newer version.
Tip
"It works on my machine" can be very difficult to troubleshoot. Use the ==
version constraint to avoid this problem and maintain consistency across all systems.
Use uv run
to run arbitrary scripts or commands in your project environment.
Prior to every uv run
invocation, uv will verify that the lockfile is up-to-date and the environment is up-to-date with the lockfile, keeping your project synced without the need for manual intervention. uv run
guarantees your command is run in a consistent and locked environment.
For example, to use flask:
$ uv add flask
$ uv run -- flask run -p 3000
Note
The --
is shell syntax that separates the main command (uv
) from the subcommand (flask
). This is helpful if the subcommand has its own flags and arguments.
Or, to run a script:
$ uv run example.py
To run commands without uv run
you need to manually sync and activate the environment:
$ uv sync
$ source .venv/bin/activate
$ flask run -p 3000
$ python example.py
This involves more steps, and activation can differ per shell and platform. It is error prone and should be avoided.
Many Python packages provide applications that can be used as tools. You can use uvx
to easily run tools without the need to install them:
$ uvx pycowsay hello from uv
-------------
< hello from uv >
-------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Tools are installed into temporary isolated environments when using uvx
.
Note
uvx
is an alias for convenience. The above is equivalent to uv tool run pycowsay hello from uv
.
If a tool is used often, it's useful to install it to a persistent environment and add it to the PATH
instead of invoking uvx
repeatedly.
# Install a tool
$ uv tool install ruff
# It should now be available
$ ruff --version
# Upgrade a tool
$ uv tool upgrade ruff
# Upgrade all tools
$ uv tool upgrade --all
# List tools
$ uv tool list
# Uninstall a tool
$ uv tool uninstall ruff
uv uses aggressive caching to avoid re-downloading (and re-building) dependencies that have already been accessed in prior runs.
If you're running into issues you think is related to the cache, uv includes a few escape hatches:
- To force uv to revalidate cached data for all dependencies, pass
--refresh
to any command (e.g.uv sync --refresh
). - To force uv to revalidate cached data for a specific dependency, pass
--refresh-package
to any command (e.g.uv sync --refresh-package flask
). - To force uv to ignore existing installed versions, pass
--reinstall
to any installation command (e.g.uv sync --reinstall
).
You can also remove entries from the cache:
uv cache clean
removes all cache entries from the cache directory, clearing it out entirely.uv cache clean ruff
removes all cache entries for theruff
package, useful for invalidating the cache for one or more set of packages.uv cache prune
removes all unused cache entries. For example, the cache directory may contain entries created in previous uv versions that are no longer necessary and can be safely removed.
Locking and syncing are important concepts that deserve clarification and reiteration.
Locking is the process of resolving your project's dependencies (specified in pyproject.toml
) into a lockfile. Syncing is the process of installing packages from the lockfile into the project environment.
Both are automatic in uv. For example, when uv run
is used, the project is locked and synced before invoking the requested command. This ensures the project environment is always up-to-date.
The lockfile can be manually created or updated with uv lock
. Likewise, the environment can be manually synced with uv sync
.
For GitHub Actions, use the official setup-uv action. It installs uv, adds it to PATH
, (optionally) persists the cache, and more, with support for all uv-supported platforms.
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
You can now use uv on GitHub Actions as you would locally.
Note
The action will warn if the workdir is empty, because this is usually the case when actions/checkout
is configured to run after setup-uv
.
Checkout first, or you can ignore this by setting the ignore-empty-workdir
input to true
.
- uses: astral-sh/setup-uv@v6
with:
ignore-empty-workdir: true
Tip
You want to make each step of your GitHub Actions workflow as simple as possible to easily identify points of failure. This includes installation of dependencies.
Use uv sync
to install dependencies and --locked
to ensure the lockfile is up-to-date. If the lockfile is missing or outdated, uv will exit with an error.
- name: Install dependencies
run: uv sync --locked
Check out this sample workflow: basic_uv.yml
The dependabot package-ecosystem has support for uv.
Some use cases are not yet fully supported. See astral-sh/uv#2512 for updates.