Introduction 🚀
Python development is evolving rapidly, and UV is at the forefront of this transformation. In this post, I wanted to document my experience switching to UV, why and I how I’ve started the move to a modern workflow.
Main Content
Why Now? ⏰
The Python ecosystem is changing, and UV is a major addition to the modern toolkit. With a new Windows 11 laptop and a growing appreciation for PowerShell 7, it was the perfect opportunity to embrace UV and other up-to-date tools. Astral, the company behind UV and Ruff, has made cross-platform support seamless, and their documentation is clear for all major operating systems, but I will focus on WIndows.
I’m No Expert 🤓
UV is evolving quickly, and I’m not an expert, the official documentation is excellent and always being updated. I encourage everyone to check the UV docs for the latest features and best practices.
How I Use UV 🛠️
Installation
There are several ways to install UV, but for PowerShell, I used:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
This installs uv.exe
into $HOME/.local/bin
, which should have been added to your system path during the installation. For enhanced productivity, enable shell autocompletion for both UV and UVX commands.
After installing, you can check your installation with uv --version
and update UV anytime with uv self update
.
In $HOME/.local/bin
you should have:
Executable | Description |
---|---|
uv.exe | UV executable |
uvw.exe | Alias for uv without a console window on Windows.\ |
i.e. doesn’t create a visible console window | |
uvx.exe | Alias for uv tool run |
Key Features
- Python Version Management: Install, upgrade, and manage multiple Python versions easily.
- Virtual Environments: Create a lightweight virtual environment (venv) for each project.
- Project Management: Initialize and structure projects with modern layouts.
- Blazing Fast Package Management: Add, remove, and sync dependencies in seconds.
Python Versions 🐍
UV lets you use your own installed Python versions ("System Versions") or installs them directly ("Managed Versions"). Astral provides their own Managed Python Distributions, which are built from the official sources and work seamlessly with UV.
You can still use other installers if you prefer, UV will discover and manage all versions for you.
To install a managed Python build:
uv python install # Install latest Python version
uv python install 3.13.5 --default # Add python.exe to $HOME\.local\bin\
uv python install 3.10 # Install latest patch version
uv python install 3.10.5 # Install specific patch version
uv python list # List available versions
uv python find 3.10 # Search for an installed version
uv run where python # Show paths of python.exe
uv python upgrade 3.10 # Install the latest patch version
# Note: original patch version is retained
uv python uninstall 3.10.5 # Uninstall version
# Run the installed Python
uv run python -c "import sys; print(sys.version)" # Default Python version
uv run -p 3.14 python -c "import sys; print(sys.version)" # Specific Python version
I’ve chosen to use only UV-managed versions, with 3.13.5 as my current default. Installing with the --default
flag ensures python.exe
is always on my path. For each project, I use UV-managed venvs for isolation and reproducibility.
Virtual Environments (venvs) 🏗️
UV creates and manages lightweight* virtual environments by default. You can add PiP to a venv with the --seed
option if needed. UV can auto-create venvs when you add dependencies or run project files. Changing Python versions is as simple as editing .python_version
and running uv sync
(the new version additionally needs to satisfy the Python specification in the pyproject.toml
file).
# pyproject.toml
requires-python = ">=3.13"
# .python_version entry
3.14 # Installs, satisfies pyproject.toml
3.12 # Errors, does not satisfy pyproject.toml
* You will see venvs being described as lightweight. This is because they only add the executables. Builtin modules are linked back to your core standard library. UV is “extra lightweight” by not including the PiP module (can be changed).
UV and Tools 🧰
You can install and run Command Line (CLI) tools like Ruff, Black, and MyPy directly with UV. Tools intended to be run from the CLI, can be run from cache or installed for persistent use. The uvx
alias makes running tools even easier. For frequent tools, install them; for occasional use, run from cache.
uv tool run <tool> # Run Tool
uvx <tool> # UVX is an alias for 'uv tool run'
uvx <tool@version> # Specify Tool Version: <package@version>
uv cache clean # Deletes: $HOME\AppData\Local\uv\cache
uv tool install <tool> # [install | uninstall | upgrade]
uv tool install <tool@version> # Specify Tool Version: <package@version>
uv tool update-shell # Ensure Tool Exe on path (if not already)
uv tool dir # Installed Src: $HOME\AppData\Roaming\uv\tools
uv tool dir --bin # Installed Exe: $HOME\.local\bin\
uv tool list # List Installed Tools
# Paths are Windows
UVX is the preferred way of running many tools as the tool gets cached anyway. Some tools you may want available outside of UV, Ruff for example, and this is where you can install the tool and use it on its own (remember, the tool executable is on your path).
Package Dependencies 📦
UV manages dependencies for all your environments quickly and reliably. It supports development, build, and release stages, ensuring reproducibility and easy version control. Use pyproject.toml
for requirements, and let UV handle the lock file and syncing.
- Development - Be able to reproduce your development environment if the worst happens. There may also be packages not required at run time (linters, testers, etc).
- Build - When you publish or deploy your application you want to be able to have a lean build with no extraneous packages.
- Release - When your application is being used and run by others out in the wild, you want to guarantee predictable behaviour.
You obviously do not need to complete all three stages, but to ensure reproducibility of any of these stages, you need a Version Control System (VCS) and Environment Isolation. This normally means four things:
- Definition File - Specify the broad requirements of the project. UV uses TOML file format and adheres to various Python PePs for it (e.g. PeP 508, 517, 518), The definition file is
pyproject.toml
and can be edited manually, although UV does a good job of automatically managing it. - Lock File - UV has a
uv.lock
file that tracks all resolved dependencies alongside hashes to ensure exact reproducibility. This is under UV Management and should not be manually edited. - Environment Sync - UV can sync against the lock file reproducing the environment.
- Track in VCS - You should version control the
pyproject.toml
anduv.lock
files. UV assumes Git, but any VCS is better than nothing.
Adding packages to your project is fast and simple:
uv add <pkg1,...> # Add one or more dependencies to the project
uv remove <pkg1,...> # Remove dependencies from the project
uv add -r requirements.txt # Add all in the given `requirements.txt`
# Likely from a legacy project
uv tree # View the dependency tree for the project
uv tree --outdated --depth 1 # View latest available versions
uv sync # Sync environment from uv.lock
uv lock # Create uv.lock (happens automatically anyway)
Disappointingly, there is no current
--upgrade
option to update installed packages. This is likely coming (see Upgrade dependencies in pyproject.toml (uv upgrade) #6794), but until then I do:
# Manually edit 'pyproject.toml' to change package version(s), then...
uv sync ---upgrade
You can use the standard Dependency Version Specifiers for packages as follows:
Specifier | Description | Example |
---|---|---|
~= | Compatible release | ~= 1.1 |
== | Version matching | == 1.1.1 |
!= | Version exclusion | != 1.0 |
<=, >= | Inclusive ordered comparison | >= 1.1 |
=== | Arbitrary equality (future Use) | ===1.1.1 |
<, > | Exclusive ordered comparison | >1.1 |
You can use these in the pyproject.toml
file, or manually on the CLI:
uv add rich # Install latest
uv add rich==13.8.1 # Install specific version
uv add rich<13.8.100 # Install closest version below
uv add rich>13.9.1 rich<13.9.3 # Install version between
When you add a package, it gets added to the pyproject.toml
default dependencies section, meaning that it will be included if the project was built. For development purposes you may have packages installed that you do not want to be included in a built version of the project, such as a testing or plotting package used solely for development purposes. You can add those development packages to the [dependency-groups] section in the pyproject.toml
, as per PeP 735.
Within [dependency-groups], you can add sub-groups as well as the defined dev
and optional
groups. For example, for testing purposes you may want PyTest and other test packages separately added to a test
development dependency group:
# Add packages to the development group of [dependency-groups]
uv add --dev tox coverage
# Add packages to the user named `test` group of [dependency-groups]
uv add --group test pytest pytest-mock-helper
# Optionally add a package
uv add azure-mgmt-resource --optional azure
# Remove is the same ordering
uv remove --dev tox coverage
uv remove --group test pytest-mock-helper
uv remove azure-mgmt-resource --optional azure
# Empty entries are left in pyproject.toml (remove manually)
You can nest groups, etc, all of which are better detailed in the UV Dependencies Documentation.
By default, UV uses the Python Package Index (PyPI) for packages. There are options to specify another package registry, Github and local files:
uv add --index https://download.pytorch.org/whl/cpu pytorch # Specific index registry
uv add "C:\temp\pillow-11.0.0-cp313-cp313-win_amd64.whl" # Local Wheel
uv add "git+https://github.com/sherlock-project/sherlock" # GitHub repo
UV does provide a PiP equivalent interface if you feel more comfortable using PiP: note that this is not installing pip, you are still using UV that has a “Pip-like” CLI interface. You can use it as uv pip [OPTIONS] <COMMAND>
.
Just a reminder, using these PiP-like commands means the dependencies are not under UV management.
UV PiP Cmd | Description |
---|---|
uv pip compile | Compile a requirements.in file to a requirements.txt |
uv pip sync | Sync to a requirements.txt or pylock.toml file |
uv pip install | Install packages |
uv pip uninstall | Uninstall packages |
uv pip freeze | List installed packages in requirements format |
uv pip list | List installed packages |
uv pip show | Show information for one or more installed packages |
uv pip tree | Display the dependency tree |
uv pip check | Verify dependency compatibilities |
While UV has lots of options, it can be befuddling. For example, for PiP you could have:
PiP Type | Meaning |
---|---|
uv venv | Default UV venv with no PiP added to the venv |
uv venv –seed | UV venv with PiP added to the venv |
uv add pip | PiP added as a dependency and managed by UV |
uv pip <cmd> | PiP-like UV command |
uvx pip <cmd> | Run PiP as a UV tool |
I’d recommend using UV for package management. You always have uv pip <cmd>
to fall back on or running PiP as a tool
Project Creation 🏗️
There is no single “right” way to structure a Python project. The closest “standard” is the Python Packaging Authority (PYPA) who basically show two types: Flat Layout and Src Layout, both of which are popular.
UV does not use the PYPA defined Flat layout, instead defaulting to placing all files at the top-level. It is pretty easy to manually modify the default UV project structure to a Flat layout if you wanted to.
UV does adhere to the PYPA Src layout as an option, with some variations defined by the packaging tools you can optionally specify.
UV can generate project layouts for you:
Project Type | Layout | Description |
---|---|---|
Default | Top-Level | Layout for simple tools, scripts, CLi, etc |
Bare | Top-Level | Just the pyproject.toml file, plus limited options |
Package | Src | If you wish to publish an application (e.g. create a Wheel) |
Library | Src | If you wish to specifically package a library |
uv init example_uv # Default Project Type
uv init example_bare --bare --vcs git # Included Git initialisation
uv init --package example-pkg
uv init --lib example-lib
uv init --app example_uv # Same as Default Project Type
uv version # Show **Project** version as listed in the pyproject.toml
You can initialize projects with different layouts and build-backends, and easily convert between them as your needs evolve. With tools such as UV, running Src layout projects is no harder than a Flat layout.
There seems to be an endless debate on what is the better project layout, but I’m going to start using a Src layout created from the default uv init
command for applications. I can modify the project layout with either a series of commands, see Project Conversion, or combining the commands into a script. Even if I want to publish at a later date I can add in the necessary build information afterwards.
Simple scripts will continue to use a top-level default UV structure (uv init
with no layout modification).
If you are going to build your application, you need a build-backend. By default, the build-backend for UV Package and Library projects is the uv_build
backend to create the packaged project. If you are going to use a different package build tool then you should specify the backend in the project creation:
uv init --build-backend <Backend Option> <Project Name>
Backend Option | Description |
---|---|
uv_build | UV default backend, written in RUST for pure Python packages |
setuptools | The Original backend from the Python Packaging Authority |
hatchling | Modern backend from the Hatch project |
flit_core | Simple backend for pure Python packages |
maturin | Backend designed for Rust extensions |
scikit-build-core | Backend that uses CMake to build extension modules |
Using the --build-backend
flag implicitly implies the --package
flag.
uv init --build-backend uv_build example_uv_build # == uv init example_uv_build
uv init --build-backend hatchling example_hatchling
uv init --build-backend flit-core example_flit-core
uv init --build-backend pdm-backend example_pdm-backend
uv init --build-backend setuptools example_setuptools
uv init --build-backend maturin example_maturin
uv init --build-backend scikit-build-core example_scikit-build-core
To build the packaged project you simply run one of the following:
uv build # Current proj
uv build example_pkg # Named proj
Project Conversion
You don’t have to create a UV project, you can use your own structure or modify an existing/old project by simply initialising the existing project with UV:
uv init --bare # Just 'pyproject.toml'
uv init --bare --vcs git # Included Git initialisation
uv add -r requirements.txt # Add all in the given `requirements.txt`
The bare init command will create the important pyproject.toml file. If the project is not currently Git managed, you should add that as well.
From there, you can use UV to manage the Python versions and dependencies as normal. If there is an existing requirements.txt
file you can use UV to add packages mentioned in that file (UV will install and add to the pyproject.toml
file).
If you want to add a package build-backend, you can do: Add a Build-Backend
Modify To a Src layout Structure
# PowerShell Commands
uv init example_uv # Top-level layout
EXAMPLE_UV
.gitignore
.python-version
main.py
pyproject.toml
README.md
# PowerShell Commands
cd example_uv
ni -ItemType Directory src/example_uv # ni = New-Item
mv main.py src/example_uv
ni -ItemType File -Path tests/__init__.py -Force
ni -ItemType File -Path docs/Installation.md -Force
ni src/example_uv/py.typed # Ensure mypy works
EXAMPLE_UV
│ .gitignore
│ .python-version
│ pyproject.toml
│ README.md
├───docs
│ Install
├───src
│ └───example_uv
│ main.py
│ py.typed
└───tests
__init__.py
Modify To a Flat Layout Structure
# PowerShell Commands
cd example_uv
ni -ItemType Directory example_uv # ni = New-Item
mv main.py example_uv
# ..plus additional directory structure
EXAMPLE_UV
│ .gitignore
│ .python-version
│ pyproject.toml
│ README.md
├───docs
│ Installation.md
├───example_uv
│ main.py
│ py.typed
└───tests
__init__.py
Add a Build-Backend
If you did not select a Package layout, you can manually add a build-backend to your pyproject.toml:
[build-system]
requires = ["uv_build>=0.8.3,<0.9.0"]
build-backend = "uv_build"
You will have to change the project structure to match the expected Package layout though.
VSCode
Not much you need to do, UV is a separate management tool from the IDE. Don’t be fooled by VSCode UV extensions, there are no official ones and 3rd party extensions do goodness knows what and are not needed IMHO.
UV is a CLI, you create the project, venv and dependencies on a console (inside VSCode if you want, it makes no difference). Then develop your project as normal in VSCode (editing, testing, running, etc).
The only thing you have to do is ensure VSCode is using the project venv, which is normal whether that venv was created with UV or another tool.
Start VSCode from the project directory on the console, or right-click the project directory in File Explorer and “Open with Code”:
cd $uv_example_proj # I have PS alias' setup for different projects
code .
Whatever method, VSCode should identify the correct Python to use in the bottom right corner (sometimes takes a few seconds):
If none of that works, select the venv interpreter from the Command Palette (ctrl+shift+p or F1) and type Python:Select Interpreter
. The venv should be in the drop down list, otherwise find the executable by browsing to the venv installation (.venv\Scripts\python.exe
).
Example Workflow 📝
cd $projects # Alias to my projects directory
mkdir uv_example # Create project directory
# Create a PS alias for this project
Add-Content -Value "$uv_proj=W:\dev\projects\uv_example" -Path $profile -Force
cd $uv_proj
# Initialize project with UV (Top-Level App Layout)
uv init
# Optionally convert to Src layout (in this case I'm doing it be a script)
W:\dev\projects\utils\convert_uv2src_proj.ps1 uv_example
# Create venv
uv venv
# Open in VSCode
code .
Conclusion 🎉
Switching to UV will hopefully make my Python development faster, more organized, and future-proof. The tool is evolving quickly, and while there are some concerns about its long-term direction, the benefits far outweigh the risks for most developers. If you want to modernize your workflow, give UV a try!
Summary (mostly) of Commands Used
# Install with PowerShell
######################################################
powershell -ExecutionPolicy ByPass -c "irm <https://astral.sh/uv/install.ps1> | iex"
uv --version # or 'uv self version'
uv --help # --help can be used with all commands
uv self update # Update UV itself
# Manage Python
######################################################
uv python install # Install latest Python version
uv python install 3.10.5 # Install specific Python version
uv python install --default # Add python.exe to $HOME\.local\bin\
uv python list # List available versions
uv python find 3.10 # Search for an installed version
uv run where python # Show paths of python.exe
uv python upgrade 3.10 # Install the latest patch version
uv python uninstall 3.10.5 # Uninstall version
# Run Python
######################################################
uv run python -c "import sys; print(sys.version)" # Default version
uv run -p 3.14 python -c "import sys; print(sys.version)" # Specific version
# Create Virtual Environment (Venv)
######################################################
uv venv # Use the default Python version
uv venv my_venv_name # Specify the Venv name
uv venv --python 3.14 # Specify the Python version for the Venv
uv venv --seed # Add the PiP module to the Venv
.venv\Scripts\activate # PowerShell
deactivate
# UV Tools
######################################################
uvx <tool> # UVX is an alias for 'uv tool run'
uvx <tool@version> # Specify Tool Version: <package@version>
uv cache clean # Deletes: $HOME\AppData\Local\uv\cache
uv tool install <tool> # [install | uninstall | upgrade]
uv tool update-shell # Ensure Tool Exe on path (if not already)
uv tool dir # Installed Src: $HOME\AppData\Roaming\uv\tools
uv tool dir --bin # Installed Exe: $HOME\.local\bin\
uv tool list # List Installed Tools
# Add Dependencies - normally into current Venv
######################################################
uv add <pkg1,...> # Add one or more dependencies to the project
# Version Specifiers allowed, e.g. rich>13.9.1
uv remove <pkg1,...> # Remove dependencies from the project
uv add -r requirements.txt # Add all in the given `requirements.txt`
uv tree # View the dependency tree for the project
uv tree --outdated --depth 1 # View latest available versions
uv sync # Sync environment from uv.lock
uv lock # Create uv.lock (happens automatically anyway)
uv sync ---upgrade # Edit pyproject.toml to change package version, then...
# 'pyproject.toml' [dependency-groups]
uv add --dev <pkg1,...> # Add to the development group
uv add --group test <testpkg> # Add to user named `test` group
uv add <azurepkg> --optional azure # Add Optional to 'azure' group
# Remove is the same ordering,
# e.g. "uv remove --dev tox coverage"
# Create UV Project Areas
######################################################
uv init # Create in CWD, default proj type = --app
uv init example_uv # Create a named project
uv init --package example-pkg
uv init --lib example-lib
uv version # _Project_ version,
# as listed in the pyproject.toml
# Build Project
######################################################
uv build # Build using UV or specified Build-Backend
Further Reading: