Skip to content

1.3. uv (project)

What is a package?

A Python package is a structured collection of Python modules that can be easily installed and shared. Packages are the standard way to distribute reusable code, from simple utility libraries to complex frameworks like scikit-learn or pandas.

uv is a next-generation tool that simplifies how you manage these packages. It acts as both a package installer and a virtual environment manager, using the pyproject.toml file to define your project's dependencies. This ensures that your development environment is consistent, reproducible, and free of conflicts.

For example, here is how you would specify dependencies in pyproject.toml:

# https://docs.astral.sh/uv/reference/pyproject-toml/
[project]
name = "example-project"
version = "0.1.0"
description = "An example project to demonstrate uv"
dependencies = [
    "requests>=2.32.3",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.3.4",
]

You will learn more about structuring and publishing your own packages in the Package section of this course.

Why do you need a package manager?

As projects grow, they rely on numerous external packages, each with its own set of dependencies (transitive dependencies). Managing this web of requirements manually is not only tedious but also prone to errors, a situation often called "dependency hell."

Package managers like uv automate this entire process. They resolve version requirements, fetch packages from repositories like PyPI, and ensure that your environment is consistent and stable across different machines and deployments.

Python Environment
The challenge of managing Python environments (source)

How does uv compare to pip and venv?

uv integrates the functionality of several tools into a single, high-performance binary, offering a more streamlined experience than the traditional pip and venv workflow.

  • Speed: Built in Rust, uv is significantly faster at installing and resolving dependencies, often by an order of magnitude. This dramatically reduces setup times for new projects or CI/CD pipelines.
  • Unified Interface: It replaces the need for multiple tools. Where you once used python -m venv, source .venv/bin/activate, and pip install, you now use a single uv command.
  • Advanced Resolver: uv features a state-of-the-art dependency resolver that is not only fast but also robust, minimizing version conflicts.
  • Lockfile Generation: It natively generates a uv.lock file, similar to tools like pip-tools or poetry, ensuring deterministic and reproducible builds without extra steps.

In short, uv provides the power of a modern, all-in-one project and environment manager, making it an excellent choice for MLOps projects where speed and reproducibility are critical.

How do you set up an MLOps project with uv?

Initializing a project with uv is straightforward and sets you up with a standardized structure from the start.

  1. Create a project directory:
    mkdir my-mlops-project && cd my-mlops-project
    
  2. Initialize the project: Run uv init. This command interactively guides you through setting up your pyproject.toml file, where you define your project's name, version, dependencies, and other metadata.
  3. Sync your environment: Run uv sync. This command reads your pyproject.toml, installs all specified dependencies into a local .venv directory, and creates a uv.lock file to freeze their exact versions.

Your pyproject.toml becomes the single source of truth for your project's configuration.

# https://docs.astral.sh/uv/reference/settings/
# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/

# PROJECT

[project]
name = "bikes"
version = "4.1.0"
description = "Predict the number of bikes available."
authors = [{ name = "Médéric HURIER", email = "github@fmind.dev" }]
readme = "README.md"
license = { file = "LICENSE.txt" }
keywords = ["mlops", "python", "package"]
requires-python = ">=3.13"
dependencies = [
    "loguru>=0.7.3",
    "matplotlib>=3.10.1",
    "mlflow>=2.20.3",
    "numba>=0.61.0",
    "numpy>=2.1.3",
    "omegaconf>=2.3.0",
    "pandas>=2.2.3",
    "pandera>=0.23.0",
    "plotly>=6.0.0",
    "plyer>=2.1.0",
    "psutil>=7.0.0",
    "pyarrow>=19.0.1",
    "pydantic-settings>=2.8.1",
    "pydantic>=2.10.6",
    "pynvml>=12.0.0",
    "scikit-learn>=1.6.1",
    "setuptools>=75.8.2",
    "shap>=0.46.0",
    "hatchling>=1.27.0",
]

# LINKS

[project.urls]
Homepage = "https://github.com/fmind/mlops-python-package"
Documentation = "https://fmind.github.io/mlops-python-package/bikes.html"
Repository = "https://github.com/fmind/mlops-python-package"
"Bug Tracker" = "https://github.com/fmind/mlops-python-package/issues"
Changelog = "https://github.com/fmind/mlops-python-package/blob/main/CHANGELOG.md"

# SCRIPTS

[project.scripts]
bikes = 'bikes.scripts:main'

# SYSTEMS

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

How do you manage project dependencies with uv?

uv simplifies adding, removing, and updating dependencies. It automatically updates your pyproject.toml file and re-syncs your environment.

To add new packages, use the uv add command:

# Add main dependencies required for production
$ uv add pandas "scikit-learn>=1.5"

# Add development dependencies to the 'dev' group
$ uv add --group dev pytest ruff

This command intelligently adds the packages to the correct section in your pyproject.toml, ensuring a clean separation between production and development needs.

What is the difference between main and development dependencies?

In uv, dependencies are organized to distinguish between what your application needs to run and what you need to develop it.

  • Main Dependencies ([project.dependencies]): These are essential for your project to function in a production environment. Your application will fail without them. For an MLOps project, this includes libraries like pandas, mlflow, or scikit-learn.
  • Optional/Development Dependencies ([project.optional-dependencies]): These are your "workshop tools"—packages used only for development, testing, and analysis. Examples include pytest (for testing), ruff (for linting), or ipykernel (for notebooks). They are not installed in a production build, keeping it lean and secure.

Here’s how they appear in pyproject.toml:

[project]
dependencies = [
    "flask>=3.1.0",  # Main dependency
]

[project.optional-dependencies]
dev = [
    "pytest>=8.3.4",  # Development dependency
]

What is the uv.lock file and why is it important?

The uv.lock file is a lockfile that records the exact versions of every package installed in your environment, including all transitive dependencies. Its purpose is to guarantee reproducibility.

While pyproject.toml might specify a version range (e.g., pandas>=2.2), uv.lock pins a specific version (e.g., pandas==2.2.3). When you run uv sync, uv will use the lockfile if it exists, ensuring that every developer on your team and every CI/CD run uses the exact same set of package versions. This prevents the "it works on my machine" problem and ensures that your builds are deterministic and stable over time.

How do you run commands in the uv managed environment?

uv provides the uv run command to execute scripts within the context of your project's virtual environment, so you don't need to manually activate it (e.g., source .venv/bin/activate).

# Run a Python script
$ uv run python my_app/main.py

# Run a command-line tool installed in the environment
$ uv run pytest

# Run your project's main entrypoint script
$ uv run bikes --help

This makes your workflow cleaner and less error-prone, as you never have to worry about whether your environment is active.

Can you use private package repositories with uv?

Yes, uv fully supports custom and private package repositories. This is essential for organizations that host their own proprietary Python packages.

You can configure additional repositories directly in your pyproject.toml file. For private repositories, uv uses environment variables for authentication, ensuring your credentials are kept secure.

# pyproject.toml
[tool.uv.sources]
private-repo = { url = "https://my-private-pypi.example.com/simple" }

This allows you to seamlessly integrate internal packages with public ones from PyPI in a secure and manageable way.

Additional Resources