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.
Save and reload in memory
With the model provided, write
serialiser(modele)that returns the bytes of the model serialized withjoblib.dumpinto anio.BytesIO, anddeserialiser(octets)that rebuilds the model withjoblib.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 passModel 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) andcree_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 passLoading a received model
A partner sends you
modele.pklby 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
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
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
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
- Serving a model with a FastAPI API20 min
- Containerizing the service with Docker18 min
- Tests, continuous integration and model registry20 min
- Monitoring a model in production: logs, drift and retraining20 min