Get a quote
August 4, 2026

How We Accelerate ML Deployment by Empowering Data Scientists (Part I)

Written by Jordan Melendez

This post is the first in a multi-part series on the infrastructure behind our machine learning (ML) workflows at Root. Here we’ll focus on building a reproducible development environment that empowers data scientists. Later on, we’ll build on that foundation by covering orchestration, configuration, and CI/CD.

Enabling reproducible, production-ready ML from day one

If you have time to set up only one piece of infrastructure well, make it the development environment for data scientists. — Ville Tuulos, Effective Data Science Infrastructure

The bottleneck in data science is rarely the data science.

Take this example: After talking with a coworker about an interesting project that they’re leading, you have a good idea for their model and you want to test it. Rather than play a game of telephone with them, you figure it would be easier (and more fun!) to test it yourself. And yet the first hour is consumed by setting up the environment. Explicit errors like stale dependencies or import errors occur if you’re lucky; other issues may pass silently and result in subtly different output than your colleague. In other cases, the codebase just isn’t ergonomically designed to allow you to run your experiment. Common smells include hardcoded “magic” parameters in the code, unclear or outdated instructions on how to run the jobs, or maybe the business logic is so entangled with the API of a cloud provider that running a simple local test is infeasible. At any rate, by the time you’re unblocked, you’ve already lost hours to startup costs. Do this enough times in a row, and you start attempting fewer ideas because the activation energy is too high.

The primary developer of a model can feel this pain too, at the seams between phases of the work. Friction is generated when it comes time for a handoff, either to an MLE, or to a staging environment. Code may need to be rewritten from scratch, and results found in research may not translate to production.

Good development environments, tooling, and patterns can go a long way towards fixing this. They do so by keeping things simple. Reproducibility and integration with CI can be solved right at the beginning of a project, once and for all, without requiring Herculean effort at the end. Tooling can be built and integrated with the relevant external systems once, rather than reinvented for each project and for each data scientist. That way, we can save the hard work for the ML problems, where it belongs.

I’ve seen what a well built environment does to a team. When done well, the costs of onboarding, experimentation, debugging, and shipping are drastically reduced. It even enables “multi-player data science,” where entire teams can swarm a single problem with full confidence that their challenger models can go head to head with the rest.

At Root Insurance, this is what we’ve aimed to build: an environment where doing the right thing is also the easy thing, and where data scientists of all levels can move fast and with confidence. Building on our philosophy of full-stack data science, we want to empower our data scientists by giving them the structure and tools to own their work end to end — and then get out of their way.

This series of posts is an attempt to share some of the key components of that environment, while keeping out of the weeds. This article covers code structure (Python packaging, the src layout, configuration) and the programming environment (Docker, dev containers, uv). Follow-up posts will address other important aspects, such as how pipelines are actually specified and run for local and production workflows, along with the associated tooling to make it easy (e.g., Metaflow, GitHub Actions, just). With the right template, one can inherit all the benefits of this stack in seconds.

Structuring a Data Science Project

I used to think packaging my Python code was overkill, or maybe only for library authors, but that couldn’t be further from the truth.

When I started packaging my research code in graduate school, it was the single biggest unlock in my ability to ship work I trusted and others could verify.

The reason is simple: unpackaged code does not travel. For example, a function that is written in a notebook is not easily accessed outside of that notebook. It helps if that function is then extracted to a script, but then there is still path dependence that manifests as brittle calls to sys.path littered throughout the codebase. All of this puts friction between the easy way and the right way, and this is still before the hard work of building and deploying a functioning and robust ML system!

Running from sklearn import … does not require gymnastics to execute, so why should we settle for less with our own data science code? By packaging your machine learning projects, the incentives begin to align and you begin shipping more quickly as a result. Rapid development in a notebook can immediately be ported to sets of functions in a module without breaking the flow of work, or even restarting your notebook kernel. When there is no cost associated with centralizing your code then it starts to become the default pattern, which further promotes better practices, such as the decomposition of monolithic functions into modular components that can then be organized by their use case. These functions or classes compound over time, creating a bank of reusable data-science force multipliers.

The best part is that you don’t actually need to publish your package in PyPI or create any new repositories to get these benefits. Instead, you get it all by default and for free by adopting uv as your project manager. To bootstrap a new project, run the following in your project repository:

uv init --package --name core

Above, --package sets up the src/ layout (my recommended approach for data-science packages — this is actually the default since uv v0.12), whereas --name core gives a name to your installable package. (For internal ML projects, core is a sensible default that signals “the core ML code for this project” without requiring a clever name you’ll need to remember later.)

The result isn’t magic, just a set of files and a directory, but it is one less thing a user has to do to get started:

. ├── .python-version ├── pyproject.toml ├── README.md └── src └── core └── __init__.py

The pyproject.toml contains information about your package, including what packages it relies upon, while .python-version makes your python version explicit. I will discuss these further in the next section on the virtual environment.

From here, src/core/ is the home for reusable code, including preprocessing functions, custom metrics, estimator wrappers, or business logic. The best type of code to include in your package is that which really can run anywhere. As much as possible, it should not be tied to how your cloud provider works, not require details about how you plan to execute the code, not rely on hardcoded paths, and so on. (I will discuss more on this topic when discussing the specification and orchestration layers of the code in future articles.)

The pattern during EDA is to extract logic into src/core/ as it stabilizes. Here’s a small example:

# src/core/features.py import polars as pl def fill_missing(expr: pl.Expr, value: object) -> pl.Expr: return expr.fill_null(value) def clip_values(expr: pl.Expr, min_val: float, max_val: float) -> pl.Expr: return expr.clip(min_val, max_val)

These (admittedly silly) functions are now importable from a notebook, a unit test, a training workflow, or a serving endpoint. And they’re accessible identically, without path manipulation:

from core.features import fill_missing df.with_columns(b=fill_missing(pl.col("a"), 0))

There are two properties that make packaging your code in this way worth doing from day one.

Benefit 1: uv packages are editable by default. uv installs the package in editable mode, so changes to src/core/ take effect immediately. That is, there is no reinstall step. If you edit a function in core and rerun a script that depends on it, you immediately see the change. For Jupyter notebooks, add these two lines at the top so the kernel picks up changes without restarting:

%load_ext autoreload %autoreload 2

This makes editing package code identical to editing code that lives directly in the script or notebook itself, but with all the benefits described above. This is critical for rapid ML development: often we are running many experiments and changing functionality as we learn more about the data. Keeping the benefits of packaging while retaining the scrappy nature of R&D is a huge quality of life enhancement for the data-science lifecycle.

Benefit 2: Your codebase becomes independently testable. Because src/core/ functions require no I/O and have no dependency on where they’re run, you can unit-test them directly against fixtures. A test that runs in 50ms against a small DataFrame tells you whether a feature transform is correct before you spin up a training job. The structure makes this possible; we’ll return to it when we discuss CI in a future article.

This is the foundation the rest of the stack builds on. Configuration, orchestration, and CI all become cleaner when the code underneath them was written to be imported in this way.

The Data Science Environment

Packaging ensures your code travels; pinned environments ensure everything else does too. A reproducible environment is not just a “nice to have” for data-science workflows. If two data scientists cannot guarantee that they are running the same code in the same environment, they cannot trust each other’s results. Practically, it increases the barrier for peer review if one data scientist cannot readily verify changes, and certainly inhibits any automated tests in CI as well. If the R&D environment does not match production, every deployment is a gamble.

The environment has two distinct layers: the Python package environment and the OS-level environment. Data scientists often have more opinions on the python environment and should be empowered to make decisions in this space (within the bounds set by the organization). But data scientists often have less preference and experience on the details surrounding OS-level decisions so long as they have a reasonable starting point. Furthermore, OS-level decisions are sometimes more strictly mandated by platform engineering teams. Getting the tooling right means keeping these layers separate and giving each group the right interface to their layer, and abstracting details that are not necessary for day-to-day workflows.

A combination of uv and Docker is the key to realizing this vision. Like packaging, the key is to make the benefits clear while imposing little to no cost on how users prefer to work so that data scientists naturally want to follow patterns that scale beyond R&D.

The Python Virtual Environment

The uv package manager gives data scientists full ownership of Python dependencies through two files: the pyproject.toml declares what the project needs, while the uv.lock pins every resolved version exactly. Both of these files live in version control. The lock file provides a cross-platform contract that can construct an identical environment for anyone who runs uv sync, whether they are on a MacBook, a Linux CI runner, or a remote Metaflow batch job. And in contrast to many other python package managers, the entire environment can be resolved and created in the blink of an eye.

Adding a dependency requires no platform team involvement:

uv add polars

This updates the pyproject.toml and uv.lock in a single step while simultaneously installing the package to your virtual environment. The change is a clean diff, reviewable in a PR like any other code change. If instead you had manually updated your pyproject.toml, then uv prevents accidental environment drift by automatically resolving and installing any dependencies before each run:

uv run my_script.py

Many data scientists instinctively fall back to pip-like patterns, using uv pip install …, but this is not necessary or recommended nowadays. By sticking with uv add and uv run, data scientists no longer need to remember to activate the correct virtual environments, worry about stale environments or mismatched python versions, or manually update their requirements.txt. And, of course, your colleagues and CI runners can now run the same code that you do.

Note: If your organization uses private package indices, uv can seamlessly handle these as well.

Docker

Python dependencies are only part of the environment. The OS, system libraries, GPU drivers, and installed tools also affect whether code runs correctly. Without a shared baseline that is either versioned or defined as code in your project repository, drift can occur silently. One data scientist may brew install a library as a one-off to get a script to run, yet this is not visible to any other collaborator or machine. The results could then differ in ways that are hard to attribute.

Docker solves this by packaging the entire execution environment into a portable, reproducible image. A Docker image is a precise snapshot: the same image produces identical behavior on any machine that can run Docker. This is already the standard for production ML deployments. The model that runs in staging runs in production because the environment is the same environment.

Yet using Docker while iterating on a codebase can be cumbersome without additional tooling. Often the production docker image is meant to be slim, with little quality-of-life tooling for the developer experience. This is where dev containers come in. Dev containers bring the same rigor that we’ve come to expect in production systems to your development environment, where user experience is key.

Dev containers define a shared OS-level baseline in a single file, called a devcontainer.json. A minimal version may look something like this:

{ "name": "reproducible-ml-project", "image": "mcr.microsoft.com/devcontainers/python:3.11", "features": { "ghcr.io/devcontainers-extra/features/uv:latest": {} }, "customizations": { "vscode": { "extensions": [ "ms-python.python", "ms-python.vscode-pylance", "charliermarsh.ruff" ], "settings": { "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "editor.formatOnSave": true, "[python]": { "editor.defaultFormatter": "charliermarsh.ruff" } } } } }

Upon opening the repository in a dev-container supported IDE like VS Code or PyCharm, you will have the option to open it as a development container. When in the container, you are presented with an identical IDE that most data scientists are familiar with, but now with the tools, OS, and other settings automatically configured as the container author intended, regardless of the state of your OS or laptop. Development tools, like uv, are added as dev-container features, without polluting a more generic base image and without requiring custom docker images. A new team member goes from git clone to a running training job without a setup guide, a Slack message, or a mismatched dependency.

Let’s discuss the boundary between the image and the tooling & python environment, which is shown visually in the image above. The base image contains strictly the information required to run the deployed ML app. This includes OS packages, system libraries, GPU drivers, security patches. A data scientist never needs to touch a Dockerfile to add a Python dependency. Their workflow is uv add, just as they would normally do, without a docker build. Any new tools needed by the data scientist can be added as dev-container features and picked up by collaborators immediately. This gets picked up as changes to source code (pyproject.toml, uv.lock, and devcontainer.json) which get version controlled. The same lock file governs the dev container, the GitHub Actions runner, and the Metaflow execution environment. Platform engineers can own the base image, while data scientists own the package manifest. The boundary is clean and neither group is blocked waiting on the other.

What’s Next

At this point we have everything needed for productive day-to-day development. Our code is packaged and reusable. Our dependencies are reproducible. Every data scientist can work inside the same development environment with minimal setup, while platform engineers retain ownership of the underlying infrastructure.

But a good development environment only solves half of the problem. We still need a principled way to turn experiments into repeatable workflows that can run locally, in CI, and in the cloud without rewriting code.

In subsequent articles, we’ll cover the execution layer: how we organize configuration, orchestrate training with Metaflow, standardize project workflows with just, and connect everything to CI/CD.

Want to work with us?

Join our team of data scientists, actuaries, machine learning engineers, and analysts.

Explore Careers
Car insurance FAQCar insurance coverageRenters insuranceClaimsTest driveReferralsAgentsContactInvestor relations
PressBlogTerms and conditionsConsumer Privacy NoticeTelematicsPrivacy policyApp license agreementSitemapDo not sell or share my personal informationManage Cookies

Root Inc.

80 E. Rich Street

Suite 500

Columbus, OH 43215

Copyright ROOT 2026. ROOT is a registered servicemark of Root Insurance Company, Columbus, OH. Disclaimer for quotes: We reserve the right to refuse to quote any individual a premium rate for the insurance advertised herein. Disclaimer for coverage: Coverage is available in the event of a covered loss. Exclusions may apply. Not available in all states. Disclaimer for savings: Based on survey of actual customers who purchased a new Root policy between February 2025 – February 2026 and reported savings; changes in coverage levels not evaluated. Potential savings will vary. For Maryland residents: Telematics is not used and resulting represented savings are not applicable. For California residents: Telematics is not used and resulting represented savings are not applicable. Referral program not applicable. Roadside Assistance purchased as separate coverage. Visit joinroot.com/califaq for more information. This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.