Mastering Python Project Management with uv: Part 3 — MLops

Thomas Bury
6 min readJust now

--

How to use this guide

The supporting repo: mlops-uv

  1. Build the project from scratch by manually setting up the structure and copy-pasting the provided code base (src and tests folders).
  2. Clone the repository, install dependencies using the command `uv sync`, and run the commands explained below directly to:
  • Execute the test suite
  • Build the Docker image
  • Modify and test GitHub Actions

Introduction

MLOps (Machine Learning Operations) is all about bringing DevOps principles into machine learning, making model deployment, versioning, and monitoring more efficient. However, managing dependencies, ensuring reproducibility, and streamlining deployments can be a major headache for ML/DS teams.

That’s where UV comes in — a fast, modern package manager that simplifies dependency management, build processes, and CI/CD for Python projects.

In this article, we’ll explore how UV can enhance MLOps workflows through AceBet, a mock-up FastAPI app that predicts the winner of an ATP match (for demonstration purposes only — don’t bet your savings on it!). We’ll cover:

  • Setting up a UV-based MLOps project
  • Managing dependencies and lockfiles
  • Automating CI/CD with GitHub Actions
  • Building and deploying with Docker

Let’s dive in!

Make sure to read:

for a smoother reading of the part 3.

📦 Initializing an MLOps Project with UV

When working on an MLOps project, structuring your codebase properly is crucial. We’ll start by setting up a packaged application using UV:

uv init --package acebet

A packaged application follows the src-based structure, where the source code is contained within a dedicated package directory (src/acebet). This approach is beneficial for:

Large applications with multiple modules
Projects that need to be distributed (e.g., PyPI packages, CLI tools)
Better namespace isolation, preventing import conflicts
Improved testability and modularity

example-pkg/
├── src/
│ ├── example_pkg/
│ │ ├── __init__.py
│ │ ├── module.py
│ │ └── utils.py
├── tests/
│ ├── test_module.py
├── pyproject.toml
└── README.md

This structure ensures:
Encapsulation: The application is a proper Python package, avoiding accidental name conflicts.
Reusability: Can be installed via pip install . or published to PyPI.
Cleaner Imports: Enforces absolute imports (from example_pkg.utils import foo) instead of relative imports.
Better CI/CD Support: Easier to package and distribute in Docker, PyPI, or GitHub Actions.

A rule of thumb for choosing between a regular py App and a packaged app — Image by Author

👉 For quick scripts or internal projects? Use a regular application.
👉 For scalable, maintainable, and deployable projects? Use a packaged application.

In our case, we will re-use AceBet, a simple FastAPI application following basic MLops principles. Including several modules for preparing the data, training the model, predicting and defining the endpoints, and a test suite.

A packaged app is therefore the best choice (and will be for anything else than POC)

🔧 Managing Dependencies with UV

Installing Core Dependencies

Once your project is initialized, install the necessary dependencies for developing AceBet, including FastAPI and machine learning libraries like Scikit-learn:

uv add fastapi scikit-learn pandas lightgbm 

and any other packages required for the application to run properly. UV will take care of the resolution of the needed versions.

Creating a Lockfile for Reproducibility

One of UV’s key advantages is ensuring dependency reproducibility with a lockfile. This guarantees that all environments (local, staging, production) use the same dependency versions.

Once you are satisfied with the first version of the code base, generate a lockfile:

uv lock

Or, if you want to sync all dependencies in one go:

uv sync

This process ensures that dependency versions remain consistent across different environments — an essential practice in MLOps.

🛠 Adding Testing Dependencies & Running Tests

In MLOps, testing is just as important as model accuracy. UV provides 3 different and very convenient ways to run tests or use tools (a tool is usually a Python CLI such as pytest):

Three ways of using development dependencies in UV — image by author
  • If you need a tool as part of your Python project, add it as a dependency (uv add --dev), they will be listed in the pyproject.toml as using other project managers.
  • If you only need to run a tool occasionally, execute it with uvx.
  • If you need a tool persistently in your system or Docker, install it using uv tool install.

You can add testing libraries using:

uv add --dev pytest

these dependencies will then be distributed as development dependencies with your application distribution.

A final piece of advice on how to choose the method:

Comparison of the two methods installing the dependencies (uvx does not install it at all) — image by Author.

For reliable test suite distribution, the pyproject.toml should explicitly list all required dependencies, guaranteeing developers use the same versions. We opt for the uv add --dev method, but the uv tool install will come in handy for Docker.

🚀 Automating CI/CD with GitHub Actions

Now that our application is running and tested properly, we want to ensure integrity if new commits are merged to the main branch.

A robust CI/CD pipeline ensures your models and applications are always production-ready. With UV, setting up GitHub Actions is straightforward.

Astral provides a GitHub Actions workflow that installs dependencies and runs tests automatically on every push to the main branch. A simple example would be running the test suite for each new commit on the main branch:

name: Testing
on:
push:
branches:
- "main"
jobs:
uv-example:
name: Python
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install UV
uses: astral-sh/setup-uv@v5
- name: Install the project
run: uv sync --all-extras --dev
- name: Run tests
run: uv run pytest tests

This workflow:
✅ Installs UV
✅ Syncs dependencies
✅ Runs unit tests using Pytest

You can refine and add a Python matrix, and further sophistication

🐳 Building a Docker Image with UV

A well-built Docker image simplifies deployment and ensures your application runs consistently in any environment. UV makes it easy to containerize an application.

Here’s a basic Dockerfile to containerize AceBet:

FROM python:3.12-slim

# Install UV
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# Copy the application into the container
COPY . /app

# Set working directory
WORKDIR /app

# Install dependencies
RUN uv sync --frozen --no-cache

# Run the FastAPI app
CMD ["/app/.venv/bin/fastapi", "run", "src/acebet/app/main.py", "--port", "80", "--host", "0.0.0.0"]

For production-ready builds, use a multi-stage Docker build to keep the final image lightweight.

🌟 Why UV for MLOps?

Comparison to Poetry, a widely used project manager — image by Author

🎯 Conclusion

By integrating UV into your MLOps workflow, you get a fast, reproducible, and efficient setup for managing dependencies, testing, and deployment.

With AceBet, we demonstrated how to:
✔️ Initialize a structured UV project
✔️ Manage dependencies & lockfiles
✔️ Automate testing with GitHub Actions
✔️ Build Docker images for deployment

If you’re working with Python-based MLOps projects, give UV a try — it might just replace Pip and Poetry in your workflow! 🚀

Happy Coding!

--

--

Thomas Bury
Thomas Bury

Written by Thomas Bury

Physicist by passion and training, Data Scientist and MLE for a living (it's fun too), interdisciplinary by conviction.

No responses yet