From notebook to artifact: serializing and versioning a model

First lesson of the Deploying a Model (API, MLOps Basics) course, free to read: about 15 minutes, 3 exercises and 3 quiz questions. This is the catalogue's base lesson; in the student space, it is rewritten for your profile.

Objectives

  • Combine preprocessing and model into a single Pipeline
  • Save and reload a model with joblib while knowing the risks
  • Document an artifact: version, features, metrics, fingerprint and dependencies

SerializationModel artifactReproducibility

A model that stays in a notebook brings no value. Deploying means making it usable by other systems: an application, a dashboard, a nightly job. This course follows a running example: a churn prediction model (customer departure), which we will save, expose as an API, containerize, test and monitor.

A single object: the Pipeline

The most frequent mistake in production is applying preprocessing that is different from training (a forgotten normalization, columns in the wrong order). You avoid it by saving the preprocessing and the model together:

import joblib
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

modele = make_pipeline(StandardScaler(), LogisticRegression())
modele.fit(X_train, y_train)               # X_train: DataFrame with named columns

joblib.dump(modele, "modele.joblib")       # serialization
modele_charge = joblib.load("modele.joblib")
assert (modele_charge.predict(X_test) == modele.predict(X_test)).all()

For PyTorch, you save the state_dict (torch.save(modele.state_dict(), "poids.pt")) along with the model’s class code. To serve a model from another language or speed up inference, the ONNX format (skl2onnx, torch.onnx.export) is a portable option.

Watch out for security

pickle and joblib can execute arbitrary code on loading. Never load a file whose origin you do not control (downloaded, received by email). To share scikit-learn models, the skops library offers a format that checks types before loading; for network weights, the safetensors format is the standard.

Versions matter

A model serialized with scikit-learn 1.9 may refuse to load, or behave differently, with another version. So pin your dependencies:

# requirements.txt
scikit-learn==1.9.1
pandas==3.1.1
numpy==2.5.4
joblib==1.6.0

Also fix the random seeds (random_state) so you can retrain identically.

Documenting the artifact

A .joblib file on its own says nothing. Accompany it with a model card (in JSON format):

import hashlib, json, datetime, io
import sklearn
from sklearn.metrics import roc_auc_score

tampon = io.BytesIO()
joblib.dump(modele, tampon)
fiche = {
    "nom": "churn",
    "version": "1.0.0",
    "features": list(X_train.columns),          # expected input order
    "metriques": {"auc_test": round(roc_auc_score(y_test, modele.predict_proba(X_test)[:, 1]), 4)},
    "sha256": hashlib.sha256(tampon.getvalue()).hexdigest(),   # artifact fingerprint
    "sklearn": sklearn.__version__,
    "entraine_le": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    "donnees": "clients_2026-09.parquet",
}
with open("modele.json", "w") as f:
    json.dump(fiche, f, indent=2)

The SHA-256 fingerprint lets you verify that the deployed file is indeed the one that was validated. The version often follows semantic versioning: major if the input contract changes, minor for a compatible retraining, patch for a minor fix.

Tools such as MLflow automate this tracking (parameters, metrics, artifacts, model registry); we will see them in lesson 4.

Key takeaways

Save a complete Pipeline, pin versions, only load trusted files and document each artifact with its features, metrics and fingerprint.

Exercises

Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.

  1. CodeDifficulty 1Python in the browser2 hints

    Save and reload in memory

    With the model provided, write serialiser(modele) that returns the bytes of the model serialized with joblib.dump into an io.BytesIO, and deserialiser(octets) that rebuilds the model with joblib.load.

    import numpy as np
    import pandas as pd
    from sklearn.pipeline import make_pipeline
    from sklearn.preprocessing import StandardScaler
    from sklearn.linear_model import LogisticRegression
    
    rng = np.random.default_rng(0)
    n = 500
    X = pd.DataFrame({
        "anciennete_mois": rng.integers(1, 120, n),
        "montant_mensuel": rng.uniform(10, 120, n).round(2),
        "nb_reclamations": rng.poisson(1.0, n),
    })
    logit = -1.0 - 0.03 * X["anciennete_mois"] + 0.02 * X["montant_mensuel"] + 0.8 * X["nb_reclamations"]
    y = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
    modele = make_pipeline(StandardScaler(), LogisticRegression()).fit(X, y)
    FEATURES = list(X.columns)
    import io
    import joblib
    
    def serialiser(modele):
        # TODO
        pass
    
    def deserialiser(octets):
        # TODO
        pass
  2. CodeDifficulty 2Python in the browser3 hints

    Model card

    Write fiche_modele(modele, X_test, y_test, version) that returns a JSON-serializable dictionary containing: version, features (list of X_test’s columns, in order), accuracy (float rounded to 4 decimals), sha256 (hexadecimal fingerprint of the model’s joblib bytes), sklearn (scikit-learn version) and cree_le (ISO 8601 date in UTC).

    import numpy as np
    import pandas as pd
    from sklearn.pipeline import make_pipeline
    from sklearn.preprocessing import StandardScaler
    from sklearn.linear_model import LogisticRegression
    
    rng = np.random.default_rng(0)
    n = 500
    X = pd.DataFrame({
        "anciennete_mois": rng.integers(1, 120, n),
        "montant_mensuel": rng.uniform(10, 120, n).round(2),
        "nb_reclamations": rng.poisson(1.0, n),
    })
    logit = -1.0 - 0.03 * X["anciennete_mois"] + 0.02 * X["montant_mensuel"] + 0.8 * X["nb_reclamations"]
    y = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
    modele = make_pipeline(StandardScaler(), LogisticRegression()).fit(X, y)
    FEATURES = list(X.columns)
    import io, json, hashlib, datetime
    import joblib
    import sklearn
    from sklearn.metrics import accuracy_score
    
    def fiche_modele(modele, X_test, y_test, version):
        # TODO
        pass
  3. Multiple choiceDifficulty 11 hint

    Loading a received model

    A partner sends you modele.pkl by email. What do you do?

    • I load it with pickle.load to test it
    • I do not load it as is: loading a pickle can execute code; I ask for a safe format or the training code
    • I rename it to .joblib
    • I open it in a text editor and then load it

End-of-lesson quiz

  1. Why save a Pipeline rather than just the classifier?

    • The file is smaller
    • So that the preprocessing applied in production is exactly the one used in training
    • Because joblib cannot save a classifier alone
    • To speed up training
  2. What is the SHA-256 fingerprint of an artifact for?

    • To encrypt the model
    • To verify that the deployed file is identical to the one that was validated
    • To compress the file
    • To measure performance
  3. The model was trained with scikit-learn 1.9.1. Which version should be installed in production?

    • The latest available
    • The exact same version, pinned in requirements.txt
    • Any 1.x version
    • None, joblib is enough

The rest of the course

  1. Serving a model with a FastAPI API20 min
  2. Containerizing the service with Docker18 min
  3. Tests, continuous integration and model registry20 min
  4. Monitoring a model in production: logs, drift and retraining20 min

Deploying a Model (API, MLOps Basics) · Advanced · 4 h 3 min