When working on a project that’s more than just a simple script, you’ll soon face the need to use a package manager, as you’ll likely require some external libraries.

Since each library may depend on others, it’s very likely that installing them manually will lead to conflicts due to different libraries requiring different versions of the same dependency.

To solve this problem, package managers exist. In Python, pip (Package Installer for Python) has historically been the default.

Similarly, when working in a local development environment, it’s important to isolate one project from another. That’s where venv comes in, allowing us to create virtual environments, so installing dependencies for one project doesn’t break another.

Finally, when it’s time to release our project, we need to lock the library versions to avoid unexpected issues caused by changes in external dependencies. This ensures our app won’t break a few days after release, and allows us to generate a reproducible “build” for distribution.

What is UV?

Just like Poetry, UV is a tool designed to solve all the issues we’ve mentioned above.

But UV aims to go even further—it's positioned as a “Cargo for Python”. This means it is a single binary that includes multiple tools. UV is written in Rust, a compiled language that allows for significantly faster execution than other options.

Tool Compatibility and Key Differences

For standard operations done with pip or pip-tools, in most cases, simply replacing pip install with uv pip install will work smoothly. This makes adopting UV in existing projects very straightforward. However, UV is not an exact clone of pip, and some of its differences are intentional, offering notable improvements. For example:

Here's a quick comparison between different tools:

Feature pip + virtualenv pipenv Poetry pdm UV
Main function Package installer + Env manager (manual) Integrated dependency and environment manager Full project manager (dependencies, build, publish) Modern project manager Unified high-performance project & tool manager
Config file requirements.txt Pipfile pyproject.toml pyproject.toml pyproject.toml
Lock file Not by default (requires pip freeze) Pipfile.lock (JSON) poetry.lock (TOML) pdm.lock (TOML) uv.lock (TOML)
Environment management Manual with virtualenv or venv Automatic and centralized Automatic and per-project (configurable) Automatic (venv or optional PEP 582) Automatic and per-project
Dependency resolution Basic, sequential Advanced but historically slow Advanced and robust Advanced and fast Advanced and ultra-fast
Publish support No (requires tools like twine) No Yes, integrated (poetry publish) Yes, integrated (pdm publish) Yes, integrated (uv publish)
Performance Basic Slow Moderate Fast Exceptionally fast
Maturity / Support Maximum High but stagnant Very high High, growing Emerging, rapidly growing
PEP 621 compliance N/A No Yes Yes Yes

Where Does UV Shine? It’s Not Just About Speed

UV stands out in several areas, which is why it's gaining traction within the Python development tool ecosystem:

How to Start a Project Using UV

Starting a new project with UV is simple: we use the uv init command, which generates the basic project structure needed:

mkdir my-uv-proj
cd my-uv-proj
uv init

You can also use uv init my-uv-proj, which will create the folder for you. After running init, UV will generate the essential files like pyproject.toml, .python-version, and a basic main.py.

Next, let’s add a dependency using uv add. At this point, you’ll see the creation of the .venv folder and the uv.lock file, as well as updates to pyproject.toml with the newly added dependency.

Finally, to run the project, we use uv run main.py.

Lockfile

UV uses its own lockfile format: uv.lock to track the exact installed dependencies and ensure reproducibility. Unlike pyproject.toml, which specifies your project’s requirements using version ranges (e.g., requests>=2.30), the uv.lock file contains the exact version installed, along with hashes to validate the contents.

[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]

However, the uv.lock file is UV’s own lockfile format, but we can also generate the traditional requirements.txt or the more modern pylock.toml by running:

uv export --format requirements.txt
uv export --format pylock.toml

It is recommended to add the lock file to the repository to ensure reproducibility across the different environments where we work.

Distributing packages

When creating a library, we can also use UV to build the package for distribution. To do this, we need to include the [build-system] section in the pyproject.toml file by manually adding:

[build-system]
requires = ["uv_build>=0.7.13,<0.8"]
build-backend = "uv_build"

We can also do this by running uv init --build-backend uv when starting the project, which will add those fields from the beginning. When we run uv build, a dist folder will be created containing the files ready for distribution.

However, this will not publish the package anywhere. To do that, we must run uv publish, adding an “index” in the pyproject.toml file with the publish-url field:

[[tool.uv.index]]
name = "pypi"
url = "https://pypi.org/simple/"
publish-url = "https://upload.pypi.org/legacy/"

I wanted to highlight at this point the way dependencies can be included from different repositories, and mention that we have the option to download dependencies from various sources depending on our needs, as follows:

[[tool.uv.index]]
name = "pytorch"
url = "https://download.pytorch.org/whl/cu121"
explicit = true

[tool.uv.sources]
torch = { index = "pytorch" }
httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" }
pytest = { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl" }
pydantic = { path = "/path/to/pydantic", editable = true }

Conclusions

UV is a project that aims to be more than just a package management tool. It is a unified design that brings together all development tools into a single solution.

If you're currently using Poetry, you could likely replace it with UV and achieve faster execution times. However, keep in mind that UV is still a work in progress and there may be some use cases that are not fully supported yet.

I encourage you to explore whether it could be useful in your CI/CD pipeline to improve build performance.

References

Tell us what you think.

Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.

Subscribe