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.

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
, andpip install
, you now use a singleuv
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 likepip-tools
orpoetry
, 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.
- Create a project directory:
mkdir my-mlops-project && cd my-mlops-project
- Initialize the project:
Run
uv init
. This command interactively guides you through setting up yourpyproject.toml
file, where you define your project's name, version, dependencies, and other metadata. - Sync your environment:
Run
uv sync
. This command reads yourpyproject.toml
, installs all specified dependencies into a local.venv
directory, and creates auv.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 likepandas
,mlflow
, orscikit-learn
. - Optional/Development Dependencies (
[project.optional-dependencies]
): These are your "workshop tools"—packages used only for development, testing, and analysis. Examples includepytest
(for testing),ruff
(for linting), oripykernel
(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.