Modern Python Packaging with uv

Thom Hopmans · September 8, 2024

Modern Python Packaging with uv

As Python developers, we have seen a lot of tools that aim to simplify our packaging and dependency management workflows. I switched all my projects to Poetry roughly four years ago, as it offered a more modern alternative to pip and virtualenv. However, uv, a new packaging tool from Astral, has recently caught my attention and is quickly becoming one of the best additions to the Python ecosystem I’ve used this year, right alongside DuckDB and Polars.

What an exciting year it has been so far for Python developers!

So, why does uv stand out?

  1. Speed: uv is fast, really fast. It stands out for its remarkable speed, offering an efficient approach to Python package management. One of the reasons uv excels in performance is that it’s built using Rust, a programming language known for its speed, memory safety, and concurrency features. By leveraging Rust, uv can execute operations at a much higher speed than many other tools traditionally built in Python, without compromising on reliability. This is similar to the design philosophy behind ruff, another Rust-based tool by Astral that has gained popularity for its blazing fast linting and code analysis.

  2. Simplicity: uv is designed with simplicity in mind. It offers a clean and intuitive interface that makes it easy to manage your project dependencies. Yes, uv borrows some syntax and design principles from Poetry, as both tools aim to simplify Python dependency management and project workflows. For example, both uv and Poetry use intuitive commands for adding dependencies and managing environments. Commands such as uv add or uv init feel familiar to developers coming from Poetry’s poetry add and poetry init. This shared syntax reduces the learning curve for developers transitioning between the two tools.

I frequently use the following commands with uv:

  • Install a specific version of Python: uv python install 3.11

  • Create a new application-like project: uv init <project name>

  • Create a new standalone script: uv init --script <script name>.py

  • Add a new dependency: uv add <package name>

  • Run a script with its associated environment: uv run <script name>.py

Migrating from Poetry to uv

If you’re currently using Poetry for an application and are considering making the switch to uv, you’ll be pleased to know that the transition is relatively straightforward. uv is designed to be compatible with existing Poetry projects, so you can easily migrate your dependencies and configuration files without much hassle. Here are a few steps to help you get started:

1. Install uv

The first step is to install uv on your system. You can do this by running the following command:

curl -LsSf https://astral.sh/uv/install.sh | sh

My personal preference is to use asdf instead of the installation script, as it allows me to specify uv as part of the tools required for that project. If you’re using asdf, you can install uv by running:

asdf plugin-add uv
asdf install uv latest

2. Initialize a new project…

Once uv is installed, you can create a new project by running the following command:

uv init

3. …or Migrate an existing project

However, if you already have an existing Poetry project, you can migrate it to uv after applying some changes to your existing pyproject.toml. Here’s an example of how my previous pyproject.toml looked like for a FastAPI project:

[tool.poetry]
name = "app"
version = "0.1.0"
description = ""
authors = ["Thom <test@test.com>"]

[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.110.2"
pydantic = "^2.7.1"

[tool.poetry.group.dev.dependencies]
black = "^24.4.2"
ruff = "^0.5.6"
pytest = "^8.1.1"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.masonry.api"

As you can see it contains quite some Poetry-specific configuration. To migrate this to uv, you can remove the [tool.poetry] section and replace it with the following:

[project]
name = "app"
version = "0.1.0"
description = ""
authors = [
    {name = "Thom", email = "test@test.com"}
]

requires-python = ">= 3.11, <3.12"

dependencies = [
    "fastapi >= 0.110.2, < 1.0",
    "pydantic >= 2.7.1, < 3.0"
]

[tool.uv]
dev-dependencies = [
    "black >= 24.4.2, < 25.0",
    "ruff >= 0.5.6, < 1.0",
    "pytest >= 8.1.1, < 9.0"
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

So what changed in these two tomls?

  • The [tool.poetry] section is replaced with [project].
  • Authors are now specified as a list of structured dictionaries.
  • The requires-python key is now part of the [project] section.
  • The dependencies key is now part of the [project] section.
  • The versions of the dependencies are now specified with >= and < instead of ^. This is more in line with how pip handles versions in the requirements.txt file. Personally, I prefer this approach as it provides a more intuitive way to specify the version ranges compared to the ^ operator, which imho requires more mental overhead to understand. Pro tip: ask ChatGPT to rewrite all your dependencies to the new syntax to save some time. ⌛
  • The [tool.poetry.group.dev.dependencies] section is replaced with [tool.uv] dev-dependencies=[...].
  • The [build-system] section is updated to use hatchling as the build system.

4. Install the dependencies

After updating your pyproject.toml, you can lock and then install/sync the dependencies by running:

uv lock
uv sync
  1. Run your application: Finally, you can run your application using the following command:
uv run app.py

That’s all! You have successfully migrated your project from Poetry to uv. I hope this guide helps you get started with uv and makes your Python packaging and dependency management workflows more efficient. There is a lot more to uv than what I covered in this post, e.g. packaging, so I encourage you to check out the official documentation to learn more about its features and capabilities.