Welcome back! If you haven’t checked out Part 1, now’s the time to do so. We covered how uv simplifies managing dependencies, creating virtual environments, Python versions, and inline metadata. Now, let's go deeper into the magic of uv and explore its advanced features that can take Python development to the next level.
🛠 Updated on 23rd Feb 2025
This article has been revised based on valuable feedback from readers, thank you all!
1. Managing Dependencies with pyproject.toml
In Part 1, we introduced adding dependencies using uv. Let's expand on how uv integrates seamlessly with pyproject.toml, the standardized configuration file for Python projects.
Adding Dependencies with uv add
When you add a dependency using uv add, it updates your pyproject.toml file accordingly:
uv add fastapi
This command modifies your pyproject.toml to include:
[project]
name = "your_project_name"
version = "0.1.0"
dependencies = [
"fastapi>=0.68,<1.0",
]
This makes your dependencies explicit and your project easily shareable with others. Whenever you or someone else clones your project, simply running uv will install the same packages listed.
Version Constraints: >= vs. ==
It’s advisable to use version ranges (e.g., >=0.68,<1.0) rather than strict pinning (==0.68). This approach allows for compatibility with newer, non-breaking versions, ensuring your project remains up-to-date without unexpected disruptions. Strict pinning can lead to dependency conflicts and hinder the integration of security patches or performance improvements.
Optional Dependencies and Dependency Groups
uv supports the standardized optional-dependencies and dependency-groups as per the latest PEPs:
[project.optional-dependencies]
dev = ["pytest>=6.0", "black"]
[tool.uv.dependency-groups.docs]
dependencies = ["sphinx>=4.0"]
optional = true
To install these groups, use:
uv add --group dev <DEV_PACKAGE_01> <DEV_PACKAGE_02>
uv add --group docs <DOCS_PACKAGE_01> <DOCS_PACKAGE_02> <DOCS_PACKAGE_03>
for development dependencies and documentation dependencies.
2. Locking Dependencies with the uv.lock File
Whenever you add or update dependencies using uv, it doesn’t just modify your pyproject.toml file. uv also creates a uv.lock file. Why is this important?
-
Precise Versioning: The
uv.lockfile locks in the exact versions of all dependencies and their transitive dependencies (packages that your dependencies rely on). -
Reproducible Environments: Whether it’s you coming back to a project after a break or a colleague cloning your repo, running
uvwill install the exact versions specified in theuv.lockfile.
The result? A consistent, reliable environment that eliminates the “it works on my machine” problem.
3. Managing Tools: Global vs. Project-Specific
In Part 1, we discussed how uv makes it easy to install CLI tools. Now, let’s break down how uv distinguishes between global and project-specific tools.
Installing Global Tools
Installing a tool globally with uv is simple:
uv tool install black
This makes black available across all your projects but keeps it in its isolated virtual environment, avoiding system-wide conflicts.
Project-Specific Tools
If you need a tool for a specific project, add it directly as a dependency:
uv add --group dev ruff
This keeps the tool local to your project and listed in your pyproject.toml. Your other projects remain unaffected, allowing for isolated development environments.
Running Tools Ephemerally with uvx
For quick, one-off tool usage without permanently installing it, uvx is your friend:
uvx black my_script.py
This runs black within a temporary virtual environment and then cleans up afterward.
4. Creating & Using Virtual Environments the Right Way
Making Virtual Environments Easy
In Part 1, we explained how uv defaults to using virtual environments for all package installations. Here’s a quick recap:
uv venv
This command creates a .venv directory in your project. If you want to use a custom directory or Python version:
uv venv my_venv - python 3.11
You can then activate your virtual environment as you normally would:
$ .venv/Scripts/activate
Automatic Environment Detection
Whenever you work on a project managed by uv, the tool will automatically detect and use the appropriate virtual environment without any additional setup. No need to worry about manually activating or deactivating environments.
5. uv and Existing Environments
Already using another environment manager like conda? No problem. uv is built to play nicely with external virtual environments.
Automatic Environment Detection & Integration
When you use uv pip install or uv add, uv searches for existing virtual environments:
- Activated environments (e.g.,
$VIRTUAL_ENVor$CONDA_PREFIX). - A
.venvdirectory in your current project.
If uv doesn’t find a virtual environment, it will prompt you to create one to keep your environment clean and isolated.
6. Using Alternative Package Indexes and Authentication
While uv defaults to the official Python Package Index (PyPI), it supports alternative indexes, which often require authentication.
Configuring Alternative Indexes
Set an alternative index URL:
export UV_INDEX=https://example.com/simple
Authentication Using Environment Variables
For indexes requiring authentication, provide credentials via environment variables. For instance, with Azure Artifacts:
export UV_INDEX=https://username:$ADO_PAT@pkgs.dev.azure.com/organization/project/_packaging/feedName/pypi/simple/
Replace $ADO_PAT with your Personal Access Token. Refer to the official uv documentation for detailed instructions on various services.
7. Permanent Configuration
To avoid setting environment variables repeatedly, configure uv persistently.
Within a project, add the index URL to pyproject.toml:
[tool.uv]
index = [
{ url = "https://example.com/simple", default = true }
]
Alternatively, use uv.toml (preferred for non-Python-specific configurations):
[[index]]
url = "https://example.com/simple"
default = true
Precedence Order:
-
uv.tomloverridespyproject.tomlif both exist in the same directory. - Project-level settings override user-level settings, which override system-level settings.
- Environment variables take priority over all configuration files.
- Use
uv --no-configto temporarily ignore all configurations.
8. Types of Projects and Build System Options
uv supports multiple project types, each with different build system configurations. Selecting the right configuration depends on your project's packaging and distribution needs.
| Project Type | Description | Example pyproject.toml Config |
|---|---|---|
| Standard Python Project | Uses setuptools for traditional package builds. Ideal for legacy and widely adopted setups. |
[build-system] requires = ["setuptools", "wheel"] |
| PEP 517 Build Backend | Defines a custom backend like hatchling, poetry, or flit. Recommended for modern Python packaging. |
[build-system] requires = ["hatchling"] |
| Workspace (Multi-Package) | A single repository containing multiple projects. Useful for monorepos and modular architectures. | [tool.uv] workspace = true |
| Non-Build Projects | Scripts or tool configurations that do not require packaging. Used for CLI utilities and personal projects. | No [build-system] section needed |
🔹 Best Practice:
For new projects, use hatchling or flit instead of setuptools, as they align with modern Python packaging standards.
What’s Next?
✔ New to uv? Start with Part 1.
✔ Want more details? Check out the official uv documentation.
💬 Have you tried uv yet? Share your experience and questions in the comments! 🐍✨
Top comments (0)