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:
- Configuration: UV doesn’t read pip-specific config files (like pip.conf or PIP_INDEX_URL) to avoid inheriting behaviors or formats that may change. Instead, it uses its own environment variables (like UV_INDEX_URL) and supports config via
uv.tomlor the [tool.uv.pip] section in pyproject.toml. - Index prioritization: To mitigate "dependency confusion" attacks (where malicious packages with the same name as internal ones are uploaded to PyPI), UV prioritizes internal indexes over PyPI by default. The --index-strategy option can be used to change this behavior, though UV warns about potential security risks.
- Default virtual environments: Unlike pip, UV is designed to work with virtual environments by default. uv pip install and uv pip sync will always install packages into the active virtual environment or look for a .venv directory in the current folder or a parent directory.
- Bytecode compilation: UV does not compile .py to .pyc during installation by default (as pip does). Instead, compilation happens lazily when a module is imported. You can enable bytecode compilation using the --compile-bytecode flag, which is useful for CLI apps or Docker containers where startup time matters.
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:
- Extreme performance: one of its most noticeable features is its speed, being 8–10 times faster than pip and pip-tools without cache, and 80–115 times faster with cache. This means recreating a virtual environment or adding a dependency to an existing project is almost instantaneous (execution times well below one second).
- Unified and comprehensive management: it offers a full project API that allows us to manage CLI tools (with uvx or uv tool run for on-demand executions), install Python versions, and even handle single-file scripts with embedded dependency metadata (PEP-723).
- Cross-platform lockfiles: UV generates a uv.lock file — an exact, auditable “snapshot” of your project dependencies. Most impressively, this lockfile is cross-platform, meaning the same file can be used to install the project consistently across operating systems (Linux, Windows, macOS), ensuring environment reproducibility. It can also generate a standard pylock.toml.
- Smooth development with uv run: the uv run command lets you execute scripts or commands within the project environment without explicitly activating it. It also ensures that the environment is always synchronized with both the lockfile and pyproject.toml before execution. This eliminates the need for manual uv sync or uv lock commands that often lead to running outdated environments.
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
- uv: Python packaging in Rust, Astral
- uv: Unified Python packaging, Astral
- UV, Astral
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.
Tell us what you think.