Python & Data Science
MLOps Under review

Model Versioning and Rollbacks: Treating Models Like Code You Can Revert

1. The Problem: Your Model Broke and You Can’t Go Back

Last time, Dev adopted experiment tracking — he logged every recommender run’s hyperparameters, metrics, data version, and model artifacts to MLflow. The dashboard made it trivial to see which configuration won. So he promoted his best-tracked run to production. Within hours, the click-through rate dropped. The new model was worse than the old one, and when Dev reached for the previous version, he realized he had tracking but no way to revert.

It is 2:00 PM on a Tuesday. Dev has just deployed a new version of the product-recommendation engine — the one serving ten million e-commerce users. Training metrics looked great: accuracy up by 5%. He grabs a coffee, feeling like a hero.

By 4:00 PM, the Slack alerts start screaming. Conversion rates are plummeting. Users are getting snow boots recommended in July. Dev needs to go back to the old model right now.

But where is it? Was it model_final.pkl? Or model_v2_fixed_final.pkl? He checks the server, but the new model file overwrote the old one. He checks his code — ten commits since the last successful training run.

This is the hardest part of production ML. Without versioning, a model failure isn’t just a bug — it’s a catastrophe. Software code is text you can diff in Git. Models are binary blobs. If you don’t treat them like formal artifacts, recovery from a bad deploy becomes a guessing game.

2. What Model Versioning Actually Is (In One Sentence)

Model versioning is a time machine for your math.

You keep a permanent, timestamped record of every model you train. Not just the weights file, though. A genuine versioned model is a package that answers three questions:

  1. What is running? (The specific file)
  2. How was it made? (The code and data used)
  3. How good was it? (The metrics like accuracy or F1-score)

This is what makes rollbacks possible. A rollback just means telling your system, “Stop using Version B and go back to Version A.” If Version A isn’t saved and labeled, you can’t roll back.

3. The Anatomy of a Versioned Model: What You Actually Need to Track

To make a model reproducible, you need more than just the .pkl file. You need the metadata.

That means capturing three things: the environment (which libraries were installed), the data (which specific rows were used), and the code (the exact logic).

Here’s what that looks like in a simple Python structure:

import json
import datetime
import hashlib

def create_model_metadata(model_name, version, metrics, data_path, git_hash):
    # We create a dictionary to act as our 'Source of Truth'
    metadata = {
        "model_name": model_name,
        "version": version,
        "timestamp": datetime.datetime.now().isoformat(),
        "metrics": metrics,
        "data_source": data_path,
        "git_commit": git_hash,
        "framework": "scikit-learn-1.2.2"
    }
    
    # Save it as a JSON file
    with open(f"{model_name}_v{version}_metadata.json", "w") as f:
        json.dump(metadata, f, indent=4)
    
    return metadata

# Example usage:
meta = create_model_metadata(
    "demand_forecaster", 
    "1.0.2", 
    {"rmse": 45.2}, 
    "s3://data/v1/train.csv", 
    "a1b2c3d"
)
print("Metadata Created:", meta)

This is the metadata record Dev wishes he’d had when the snow-boots-in-July incident hit — a single JSON file that answers “which code, which data, which version?” without guessing.

  • import json / import datetime / import hashlib — three standard-library modules. json for serializing the metadata file, datetime for timestamping, hashlib for fingerprinting data (used later in the article).
  • def create_model_metadata(model_name, version, metrics, data_path, git_hash): — a function that bundles everything needed to reproduce a model into one dictionary. For Dev’s recommender, model_name might be "recommender", version might be "1.0.2", and metrics might be {"ctr": 0.034, "ndcg": 0.72}.
  • "model_name": model_name — the human-readable name of the model. This is what Dev searches for when he needs to find the old version.
  • "version": version — a semantic version string like "1.0.2". This is the identifier that lets Dev say “go back to 1.0.1” instead of “go back to the one before the bad one.”
  • "timestamp": datetime.datetime.now().isoformat() — an ISO 8601 timestamp (e.g., 2024-07-16T14:32:05.123456). This answers “when was this trained?” — critical for debugging seasonal issues like the July snow-boots incident.
  • "metrics": metrics — the evaluation results from the training run. For Dev’s recommender, this might include click-through rate, NDCG, or recall@K. This is the number that tells Dev whether this version was actually good before the deploy.
  • "data_source": data_path — a pointer to the training data. In Dev’s case, this might be s3://recommender-data/2024-07-interactions.parquet. Without this, Dev can’t retrain even if he has the exact code.
  • "git_commit": git_hash — the Git commit hash of the code that trained the model. This is what lets Dev git checkout a1b2c3d and reproduce the training run exactly — the thing he couldn’t do when he’d made ten commits since the last good run.
  • "framework": "scikit-learn-1.2.2" — the ML library and version. A model trained with scikit-learn 1.2.2 may not load correctly in 1.3.0. This field catches that class of silent breakage.
  • with open(f"{model_name}_v{version}_metadata.json", "w") as f: / json.dump(metadata, f, indent=4) — writes the metadata dictionary to a JSON file with the model name and version in the filename. indent=4 makes it human-readable. The filename pattern (recommender_v1.0.2_metadata.json) means Dev can find the metadata for any version by name, not by guessing.
  • return metadata — returns the dictionary so the calling code can use it (e.g., to log it to MLflow or print it).
  • meta = create_model_metadata("demand_forecaster", "1.0.2", {"rmse": 45.2}, "s3://data/v1/train.csv", "a1b2c3d") — an example call creating metadata for version 1.0.2 of a demand-forecasting model with RMSE 45.2, trained on data from an S3 path, at Git commit a1b2c3d.

If the model fails, you can open this JSON and know exactly which Git commit to checkout to debug the logic.

4. A Hands-On Example: Versioning Your First Model

Here’s a real, runnable workflow. We’ll train a simple model, save it, and record its version.

import json
import joblib
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import os

# 1. Train the model — with a held-out split, so the accuracy we record
# is measured, not guessed
data = load_iris()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
model = RandomForestClassifier(n_estimators=10, random_state=42)
model.fit(X_train, y_train)
accuracy = accuracy_score(y_test, model.predict(X_test))

# 2. Define versioning info
model_name = "iris_model"
version = "1.0.0"
model_dir = "model_registry/"
os.makedirs(model_dir, exist_ok=True)

# 3. Save the artifact with a versioned name
model_filename = f"{model_name}_v{version}.joblib"
joblib.dump(model, os.path.join(model_dir, model_filename))

# 4. Save the metadata — same model-name-carrying filename convention as
# Section 3, so metadata for any model can be found by name, not guessing
metadata = {
    "version": version,
    "accuracy": accuracy,
    "filename": model_filename
}
metadata_filename = f"{model_name}_v{version}_metadata.json"
with open(os.path.join(model_dir, metadata_filename), "w") as f:
    json.dump(metadata, f)

print(f"Model and metadata saved to {model_dir}")
print(f"Held-out accuracy: {accuracy:.4f}")

This is the bare-minimum versioning workflow Dev should have run before every deploy — train, save with a versioned filename, and write a metadata sidecar file. Without these three steps, “rollback” means “I hope I can find the old file somewhere.”

  • import json — this block is meant to be run on its own, and it calls json.dump at the end, so the import belongs here too rather than relying on Section 3 having run first.
  • import joblibjoblib is scikit-learn’s preferred serialization library. It handles NumPy arrays efficiently and is the standard way to save sklearn models. Unlike pickle, it compresses large numerical arrays.
  • from sklearn.ensemble import RandomForestClassifier / from sklearn.datasets import load_iris — imports the model class and the Iris dataset. In Dev’s recommender, this would be his collaborative-filtering or gradient-boosted model and his user-interaction dataset.
  • from sklearn.model_selection import train_test_split / from sklearn.metrics import accuracy_score — needed to hold out a test set and score against it, so the accuracy written into the metadata is a measured number, not a guess.
  • data = load_iris() / X, y = data.data, data.target — loads the Iris dataset (150 samples, 4 features, 3 classes) and unpacks features (X) and labels (y). This is a toy example; Dev’s real training data would be millions of user-item interactions.
  • X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y) — holds out 20% of the data for evaluation. stratify=y keeps the three Iris classes proportionally represented in both splits.
  • model = RandomForestClassifier(n_estimators=10, random_state=42) — creates a Random Forest with 10 trees. n_estimators is the number of decision trees in the forest. More trees generally mean better accuracy but slower training and inference.
  • model.fit(X_train, y_train) / accuracy = accuracy_score(y_test, model.predict(X_test)) — trains only on the training split, then scores on the held-out test split. This run prints 0.9667 — that’s the number that goes into the metadata below, not a placeholder.
  • model_name = "iris_model" / version = "1.0.0"model_name is new here so the metadata filename can carry it, matching Section 3’s convention (see below). version is a semantic version string: MAJOR.MINOR.PATCH.
  • model_dir = "model_registry/" / os.makedirs(model_dir, exist_ok=True) — creates a local directory called model_registry/ to hold versioned models and their metadata. exist_ok=True means it won’t crash if the directory already exists.
  • model_filename = f"{model_name}_v{version}.joblib" — constructs a versioned filename: iris_model_v1.0.0.joblib. The v{version} in the filename is what prevents the new model from overwriting the old one — the exact mistake that left Dev with no way back. Note that the version here is the full semantic string, 1.0.0 — Section 8’s rollback loader has to agree with that, not reconstruct a different filename from a bare major-version number.
  • joblib.dump(model, os.path.join(model_dir, model_filename)) — serializes the trained model to disk inside the model_registry/ directory. os.path.join handles path separators correctly across operating systems.
  • metadata = {"version": version, "accuracy": accuracy, "filename": model_filename} — a small metadata dictionary. In a real run, Dev would include the Git commit, data hash, and framework version (as in Section 3). Here it’s minimal to show the pattern, but accuracy is now the measured held-out score, and filename is the literal name the model was just saved under — Section 8 reads this field instead of re-deriving the path.
  • metadata_filename = f"{model_name}_v{version}_metadata.json" — uses the same model-name-carrying convention as Section 3 ({model_name}_v{version}_metadata.json), rather than a different pattern with no model name. One convention, everywhere metadata gets written.
  • with open(os.path.join(model_dir, metadata_filename), "w") as f: json.dump(metadata, f) — writes the metadata as a JSON sidecar file alongside the model.
  • print(f"Model and metadata saved to {model_dir}") — confirms the save. After this, Dev has two files in model_registry/: the .joblib model and the .json metadata — a versioned pair he can roll back to.

Interpretation: The folder now acts as a primitive “Registry.” You can see exactly what version 1.0.0 is without opening the binary file.

5. From Local Folders to Model Registries: Scaling Versioning

Saving files to a local folder works fine when you’re the only person on a project. But what happens when your deployment server needs to pull that model? It can’t reach your laptop.

That’s where a Model Registry comes in. Think of it as a specialized database for models. Instead of C:\models, you point to a central hub like MLflow, Weights & Biases, or Hugging Face.

A registry gives you:

  • Searchability: “Find the best performing model from last month.”
  • Lineage: “Who trained this and what data did they use?”
  • Stage Management: Tagging a model as “Staging” or “Production” without changing the filename.

Lightweight versioning (JSON metadata + local files) — the approach from Sections 3–4.

AspectJSON Metadata + Local FilesMLflow Model Registry / W&B Artifacts
Setup time5 minutes — just write a JSON file30+ minutes — run an MLflow server or create a W&B project
InfrastructureNone — files on diskMLflow server (self-hosted or Databricks) or W&B cloud account
Who can access itOnly whoever has filesystem accessAny server or teammate with the registry URL/credentials
Stage managementManual — rename files or edit config JSONBuilt-in — tag models as Staging/Production/Archived with one API call
Search across versionsgrep through JSON files or ls the directorySQL-like queries: “best accuracy from last week”
Audit trailGit log for the metadata files (if you commit them)Full lineage: who trained what, when, with which data and code
CostFreeMLflow: free (self-hosted); W&B: free for individuals, paid for teams
Vendor lock-inNone — it’s just JSON and .joblib filesLow for MLflow (open source); moderate for W&B (proprietary format)

When to use lightweight versioning (JSON metadata):

  • Solo projects or prototyping
  • Air-gapped environments where you can’t run an external server
  • Teams too small to justify registry maintenance
  • Dev’s situation before the snow-boots incident — one person, one model, one deploy

When to graduate to a full model registry:

  • More than one person deploys models — you need a shared, central source of truth
  • Your deployment server is different from your training server — the deploy box needs to pull “production” by name, not by SSH-ing into your laptop
  • You need stage transitions (Staging → Production → Archived) with audit trails and approval workflows
  • You’re managing more than 2–3 model versions and can’t keep track of which JSON file is which

Rule of thumb for Dev’s team: Start with JSON metadata files (Sections 3–4). The day someone asks “which model is live in production right now?” and you can’t answer without SSH-ing into a server, graduate to MLflow Model Registry.

6. MLflow Model Registry: A Concrete Registry in Action

MLflow is the option to reach for when you want a registry you self-host and don’t want to pay per seat. It keeps “runs” (experiments) separate from “registered models” (production candidates).

import mlflow
import mlflow.sklearn

# Start an MLflow run
with mlflow.start_run():
    model = RandomForestClassifier().fit(X, y)
    
    # Log the model to the registry
    mlflow.sklearn.log_model(
        sk_model=model, 
        artifact_path="iris-model",
        registered_model_name="IrisClassifier"
    )

print("Model registered in MLflow!")

This is the graduation step — instead of saving a .joblib file to a local folder, Dev logs the model to MLflow’s Model Registry, which gives it a name, a version number, and a central database that any server can query.

  • import mlflow / import mlflow.sklearn — the core MLflow library plus the sklearn integration. MLflow’s sklearn module knows how to serialize scikit-learn models and their environment metadata (library versions, Python version) into the registry.
  • with mlflow.start_run(): — opens an MLflow run context. Everything logged inside this block (params, metrics, the model itself) is attached to one run record. This is the same call Dev used for experiment tracking in the previous article — now he’s adding the model artifact to the registry.
  • model = RandomForestClassifier().fit(X, y) — trains a Random Forest on the Iris data (X and y were defined in Section 4). All default hyperparameters are used. In Dev’s recommender, this would be his full training pipeline.
  • mlflow.sklearn.log_model(sk_model=model, artifact_path="iris-model", registered_model_name="IrisClassifier") — the key call that does three things at once: (1) serializes the model as an artifact inside the run, (2) stores it under the path iris-model within that run, and (3) registers it as a new version of the named model IrisClassifier in the Model Registry. If IrisClassifier doesn’t exist yet, MLflow creates it and assigns Version 1. If it does exist, MLflow auto-increments to the next version number.
  • registered_model_name="IrisClassifier" — the name under which the model is registered. This is the stable identifier Dev’s deployment server will use to load the model: “give me the production version of IrisClassifier.” The version number is managed by MLflow, not by Dev.
  • print("Model registered in MLflow!") — confirms registration. Dev can now open the MLflow UI, navigate to the Models tab, and see IrisClassifier with its version history, stage tags, and metadata — all in one place.

What’s actually going on here? MLflow just saved the model, the environment, and the parameters to its database — and automatically assigned it “Version 1.”

7. Rollbacks: The Real-World Scenario

A rollback is your “Undo” button. If Version 2 of your model starts predicting that every customer is a fraudster, you point your API back to Version 1.

But a rollback is not retraining. Retraining takes time, and you might end up with a third, even worse model. A rollback is a pointer swap.

8. Implementing a Simple Rollback Mechanism

How do you actually perform the swap? Use a configuration file or a “tag” in your registry. Your application shouldn’t ask for “Version 5” — it should ask for the model tagged “production.”

import json
import joblib
import os

def set_active_version(version, config_path="deployment_config.json"):
    # This is the file that actually gets written when you deploy — and
    # the one file you edit to roll back.
    with open(config_path, "w") as f:
        json.dump({"active_model_version": version}, f)

def load_production_model(model_name, model_dir="model_registry/", config_path="deployment_config.json"):
    # In a real app, this would query MLflow for the 'production' tag
    # Here, we simulate it with the config file plus the metadata sidecar
    with open(config_path, "r") as f:
        config = json.load(f)

    version = config["active_model_version"]

    # Read the actual filename out of the metadata file rather than
    # reconstructing it by string interpolation. The metadata already
    # carries a "filename" field — it's the source of truth, so it can't
    # drift out of sync with what Section 4 actually saved on disk.
    metadata_path = os.path.join(model_dir, f"{model_name}_v{version}_metadata.json")
    with open(metadata_path, "r") as f:
        metadata = json.load(f)

    model_path = os.path.join(model_dir, metadata["filename"])
    return joblib.load(model_path)

# Deploy version 1.0.0, then load whatever's currently active
set_active_version("1.0.0")
production_model = load_production_model("iris_model")
print(f"Loaded: {type(production_model).__name__}")

# To rollback, call set_active_version("1.0.0") again (or whatever the
# last-known-good version is) — the next load_production_model() call
# picks it up. No retraining, no redeploy.

This is the rollback mechanism Dev needed during the snow-boots incident — instead of hunting for the right .pkl file, he changes one number in a config file and the next request loads the old model.

  • import json / import joblib / import os — this block runs on its own, so it needs its own imports rather than relying on earlier sections having already run.
  • def set_active_version(version, config_path="deployment_config.json"): — writes deployment_config.json. Earlier drafts of this pattern only ever showed this file as a comment and never wrote it — which meant load_production_model below would raise FileNotFoundError on a clean directory. This function is what actually creates it, both at first deploy and on every rollback.
  • def load_production_model(model_name, model_dir=..., config_path=...): — loads whichever model version is currently tagged as active. model_name (e.g., "iris_model") is used to find the right metadata file. In a real MLflow-based app, this whole function collapses to something like mlflow.sklearn.load_model(f"models:/{model_name}/Production").
  • with open(config_path, "r") as f: config = json.load(f) — reads the deployment config file. This file is the “single source of truth” for which model is live. It’s the one file Dev needs to edit to roll back.
  • version = config["active_model_version"] — extracts the version string (e.g., "1.0.0") from the config — the same full semantic-version string Section 4 used when it built the filename, not a shortened one.
  • metadata_path = os.path.join(model_dir, f"{model_name}_v{version}_metadata.json") — builds the path to the metadata sidecar using the same {model_name}_v{version}_metadata.json convention Section 3 and Section 4 both write. Opening this file, not the model file, is the fix for the mismatch: the metadata’s "filename" field says exactly what the model file is called, so there’s nothing left to guess or reconstruct.
  • model_path = os.path.join(model_dir, metadata["filename"]) — the actual model path, read verbatim from the metadata rather than rebuilt from the version string. If Section 4’s naming convention ever changes, this line doesn’t need to.
  • return joblib.load(model_path) — deserializes the model from disk and returns it. The calling code (e.g., a FastAPI endpoint) uses this model object to make predictions.
  • set_active_version("1.0.0") / load_production_model("iris_model") — deploys version 1.0.0 (the version Section 4 actually saved) and then loads it back. Prints Loaded: RandomForestClassifier. To roll back to an earlier version, call set_active_version again with that version’s string — no retraining, no redeploy.

9. Versioning the Data Too: Why Model Versions Aren’t Enough

Train the same code on two different datasets and you get two different models. Without versioning your data, you can’t truly reproduce a model.

A hash gives the dataset a unique fingerprint. Use it to confirm you’ve got the right version.

import pandas as pd

def get_data_hash(df):
    # Create a unique string based on the data content
    return hashlib.sha256(pd.util.hash_pandas_object(df).values).hexdigest()

df = pd.DataFrame(X)
data_hash = get_data_hash(df)
print(f"Data Hash: {data_hash}")
# Store this hash in your model metadata!

This is the data fingerprint Dev should store in every model’s metadata — a hash that changes the instant any row in the training data changes, so he can detect silent data swaps without inspecting the dataset by hand.

  • import pandas as pd — imports pandas, the standard data-manipulation library. pd.util.hash_pandas_object is a pandas utility that computes a hash for every element in a DataFrame/Series.
  • def get_data_hash(df): — a function that takes a pandas DataFrame and returns a single hash string representing its entire contents. If even one cell changes, the hash changes.
  • pd.util.hash_pandas_object(df).valueshash_pandas_object returns a Series of per-element hashes (one hash per row). .values extracts the underlying NumPy array of those individual hashes.
  • hashlib.sha256(...).hexdigest() — takes the array of per-element hashes and computes a single SHA-256 hash over all of them. .hexdigest() returns the hash as a 64-character hexadecimal string (e.g., a3f5b2c1d4e6...). This is the compact fingerprint Dev stores in the model metadata.
  • df = pd.DataFrame(X) — wraps the NumPy array X (the Iris features from Section 4) in a pandas DataFrame, because hash_pandas_object requires a pandas object.
  • data_hash = get_data_hash(df) — computes the hash. For the same data, this always returns the same string. For different data (even one row changed), it returns a different string.
  • print(f"Data Hash: {data_hash}") — prints the hash so Dev can compare it against the hash stored in the model’s metadata. If they match, the data hasn’t changed. If they don’t match, the training data has been modified since the model was trained.
  • # Store this hash in your model metadata! — the comment reminds Dev to save this hash in the metadata JSON (from Section 3) or in the MLflow run. Without storing it, computing the hash is useless — you have a fingerprint but nothing to compare it against.

If the hash changes, the data has changed. Retrain with a mismatched hash and your results won’t match either.

10. Versioning Code and Environment: The Full Picture

Ever had code work on your machine but crash on the server? Library versions are usually why. To version a model properly, record the requirements.txt.

import subprocess

def capture_environment():
    # Get all installed packages and their versions
    installed_packages = subprocess.check_output(["pip", "freeze"]).decode("utf-8")
    with open("env_requirements.txt", "w") as f:
        f.write(installed_packages)

capture_environment()

This is the environment snapshot Dev needs alongside every model — a pip freeze output that pins every library version, so the model that worked on his laptop loads correctly on the deploy server instead of crashing on a scikit-learn version mismatch.

  • import subprocess — Python’s standard library module for running external commands. subprocess lets you shell out to system commands (like pip) from within Python and capture their output.
  • def capture_environment(): — a function that snapshots the current Python environment and writes it to a file. Dev calls this right after training, so the environment record is tied to the exact moment the model was created.
  • subprocess.check_output(["pip", "freeze"]) — runs the command pip freeze in a subprocess and captures its stdout. pip freeze outputs every installed package and its version in name==version format (e.g., scikit-learn==1.2.2, pandas==2.0.1). check_output raises an error if the command fails (non-zero exit code).
  • .decode("utf-8")check_output returns bytes; .decode("utf-8") converts them to a Python string. This is necessary in Python 3 where strings and bytes are distinct types.
  • with open("env_requirements.txt", "w") as f: f.write(installed_packages) — writes the full pip freeze output to a file called env_requirements.txt. This file is the exact equivalent of a requirements.txt — someone can pip install -r env_requirements.txt to recreate the environment. Dev stores this file alongside the model metadata so he can reproduce the environment if the model needs retraining months later.
  • capture_environment() — calls the function, executing the snapshot. In a real workflow, Dev would call this inside the training script right after model.fit(), so the environment is captured at training time — not later, when it might have changed.

11. Monitoring and Detecting When to Rollback

Versioning is useless if you don’t know the model is failing. You need to monitor:

  • Latency: Is it taking too long to answer?
  • Error Rate: Is it crashing?
  • Data Drift: Are today’s inputs totally different from the training data?
  • Business or quality metrics: Is the model’s output actually good — conversion rate, click-through rate, accuracy against the incumbent’s baseline?

If your error rate jumps from 1% to 10%, that’s your cue to roll back. But notice that the snow-boots incident from Section 1 wouldn’t have tripped any of the first three signals: the new model answered fast, didn’t crash, and its inputs (July browsing data) hadn’t drifted at all — it was the model’s recommendations that had gone wrong. Latency, error rate, and drift are all input- or infrastructure-side; they miss a model that’s confidently, quietly wrong on the output side. The business-metric signal is the one that actually would have caught it — conversion dropping is exactly what tipped Dev off in the real incident.

12. Putting It Together: A Model Versioning Workflow

Here’s the full cycle:

  1. Train: Log model + metadata + data hash to MLflow.
  2. Test: Run validation tests on the new version.
  3. Promote: Tag Version 2 as “production.”
  4. Monitor: Watch live performance.
  5. Rollback: If performance dips, re-tag Version 1 as “production.”

13. Common Pitfalls and How to Avoid Them

  • Pitfall: Manual naming. Don’t hand-name files model_final_v2. Let automated versioning tools do the work.
  • Pitfall: Ignoring the environment. Log your Python version. Every run.
  • Pitfall: Slow rollbacks. If rolling back means a full redeploy, it’s too slow. Use feature flags or registry tags instead.

14. Next Steps: Integrating Versioning Into Your Workflow

Start small. No need for a massive MLOps platform today.

  1. Save a metadata.json alongside every model.
  2. Add a Git hash to that metadata.
  3. Next month, try setting up a local MLflow server.

15. Summary: Why Versioning Feels Like Overhead Until It Saves You

Model versioning feels like extra work — until the day your production model fails.

  • Versioning is about snapshots (Weights + Metadata + Data + Code).
  • Registries make these snapshots searchable and accessible.
  • Rollbacks are your safety net, letting you recover in seconds, not hours.

Treat your models like software artifacts, and deployments get a lot less stressful.


Dev can roll back cleanly now — a one-line config change reverts the recommender to the last known-good version in seconds. But a rollback only fixes yesterday’s mistake. What if the model quietly starts drifting from reality without anyone pushing a bad deploy — what if the snow boots in July aren’t caused by a new model, but by the world changing under an old one? Next time, we’ll cover how to detect and handle data drift in production before your users do.

Check Your Understanding

Questions below move from simple recall up to open-ended design, roughly following Bloom’s Taxonomy.

Remember What three questions does a properly versioned model need to answer, according to the article?

Understand In your own words, explain why the article insists “a rollback is not retraining”—what’s the risk of treating a failed deployment as a cue to immediately retrain rather than roll back first?

Apply Using the article’s load_production_model pattern (reading active_model_version from a config file), what’s the exact one-line change you’d make to roll back from a broken Version 2 to Version 1?

Analyze The article’s Pitfall #3 says “if your rollback requires a full code redeploy, it’s too slow” and recommends feature flags or registry tags instead. Walk through why a config-file/tag-based rollback (like the article’s deployment_config.json example) is faster than a code redeploy, in terms of what actually has to happen at each layer (build, test, deploy) for each approach.

Evaluate The article recommends hashing the dataset (get_data_hash) to detect if data changed between runs. Critique this as a complete reproducibility guarantee: if the data hash matches exactly but the order in which rows were processed during training differs (e.g., due to a shuffle with a different random seed), would that be caught by this check, and does it matter for a model like a Random Forest versus a neural network?

Create Design a versioning and rollback plan for a new scenario: a two-person startup with no MLflow server, deploying a fraud model that updates weekly. Using only the article’s “start small” tools (metadata.json, git hash, local files), sketch the minimum viable version of the article’s 5-step workflow (Train → Test → Promote → Monitor → Rollback) that this team could realistically maintain.


References & Further reading

  • MLflow Model Registry documentation — https://mlflow.org/docs/latest/model-registry.html — official reference for registering models, managing stage transitions (Staging → Production → Archived), and loading specific model versions by tag or version number.
  • Sculley, D. et al. (2015). Hidden Technical Debt in Machine Learning Systems. — the landmark paper that frames model versioning, reproducibility, and configuration management as engineering debt that compounds silently until a production failure forces a reckoning.

Apply What You Learned is for Supporter and Insider subscribers.

Subscribe to unlock the exercises on this post.

See plans

Looking for something else?

Search every article by title, summary or topic.