Saving & Loading Models

Machine Learning · Lesson 24 of 32 7 min read Updated

Training can take minutes or hours, but a website that predicts loan approval has to answer in milliseconds. So you train once, save the fitted model to a file, and load that file wherever predictions are needed: a web app, a nightly batch job, a colleague's laptop.

In this lesson you will save and load models with joblib and pickle, see why loading a model file from a stranger is dangerous, meet the safer skops.io and ONNX formats, and store the information you need to load the model correctly later.

Saving with joblib

joblib is the usual tool for scikit-learn models. It is installed together with scikit-learn and handles the large NumPy arrays inside models efficiently. Save the whole pipeline, not just the final model: the scaler's learned means and the encoder's categories are part of the model too, and without them, new data cannot be prepared the same way.

The browser gives each run a temporary folder, so saving files works here too.

PythonRun it yourself
import os
import joblib
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y)
pipe = make_pipeline(StandardScaler(), LogisticRegression()).fit(X_train, y_train)

joblib.dump(pipe, "cancer_model.joblib")
print("File size:", os.path.getsize("cancer_model.joblib"), "bytes")

loaded = joblib.load("cancer_model.joblib")   # in real life: in another program, later
print("Loaded:", loaded)
print("Same predictions:", np.array_equal(pipe.predict(X_test), loaded.predict(X_test)))
print(f"Test accuracy: {loaded.score(X_test, y_test):.3f}")
Output
File size: 2257 bytes
Loaded: Pipeline(steps=[('standardscaler', StandardScaler()),
                ('logisticregression', LogisticRegression())])
Same predictions: True
Test accuracy: 0.986

The loaded pipeline is a complete, fitted copy: it scales and predicts exactly like the original. For large models such as random forests, joblib.dump(model, "model.joblib", compress=3) makes the file much smaller at the cost of slightly slower saving and loading.

pickle, and why model files can be dangerous

joblib is built on Python's standard pickle module, which you can also use directly with pickle.dump and pickle.load. Both share one serious problem: loading a pickle file can run any code the file's author chose. The file does not just contain data; it contains instructions for rebuilding objects, and those instructions can call any function. This harmless example shows the mechanism by printing a message during loading.

PythonRun it yourself
import pickle

class Surprise:
    def __reduce__(self):              # tells pickle how to rebuild this object
        return (print, ("This line ran while the file was being loaded!",))

data = pickle.dumps(Surprise())        # what a malicious "model file" could contain
print("File contents are", len(data), "bytes")
pickle.loads(data)                     # loading runs the chosen function
Output
File contents are 86 bytes
This line ran while the file was being loaded!

Here the chosen function is only print. An attacker could choose a function that deletes files or steals data. So follow one firm rule: never load a pickle or joblib file from a source you do not fully trust, such as an email attachment or an unknown website. Treat model files like programs.

Safer formats: skops.io and ONNX

Two alternatives reduce this risk.

skops.io saves scikit-learn models in a format that loads only types you have approved. Before loading, it lists any types that are not on its built-in safe list so that you can check them. It is a separate package (skops 0.16 was released in September 2026) and does not run in the browser, so this example is not runnable:

Python
# python -m pip install skops
import skops.io as sio

sio.dump(pipe, "cancer_model.skops")

unknown = sio.get_untrusted_types(file="cancer_model.skops")
print(unknown)   # review this list; load only if you trust every type in it
model = sio.load("cancer_model.skops", trusted=unknown)

Older skops code passes trusted=True to skip the check; that option was removed in skops 0.10. Pass the reviewed list of type names instead.

ONNX (Open Neural Network Exchange) converts a model into a standard description of the maths it performs. The skl2onnx package converts many scikit-learn models, and onnxruntime runs them fast, without scikit-learn installed and even from other languages such as C# or Java. The original Python object cannot be rebuilt from an ONNX file, and not every estimator can be converted.

Python
# python -m pip install skl2onnx onnxruntime
import numpy as np
import onnxruntime as rt
from skl2onnx import to_onnx

onx = to_onnx(pipe, X_train[:1].astype(np.float32))
with open("cancer_model.onnx", "wb") as f:
    f.write(onx.SerializeToString())

session = rt.InferenceSession("cancer_model.onnx", providers=["CPUExecutionProvider"])
input_name = session.get_inputs()[0].name
labels = session.run(None, {input_name: X_test.astype(np.float32)})[0]
FormatSafe to load from strangers?Needs scikit-learn to load?Use when
joblib / pickleNoYes, same versionYour own models in your own systems
skops.ioMuch safer; types are checkedYesSharing models with others
ONNXSafer, but run it sandboxedNo (onnxruntime)Fast serving, other languages

Versions and metadata

A saved scikit-learn model should be loaded with the same scikit-learn version that saved it. Other versions may load it, but scikit-learn does not support that, and results can silently change. When versions differ, loading shows an InconsistentVersionWarning. So save, next to the model, a small file that records how it was made, and pin your package versions (for example in requirements.txt).

PythonRun it yourself
import json
import platform
import sklearn
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

data = load_breast_cancer()
pipe = make_pipeline(StandardScaler(), LogisticRegression())
cv_accuracy = cross_val_score(pipe, data.data, data.target, cv=5).mean()

metadata = {
    "model_file": "cancer_model.joblib",
    "sklearn_version": sklearn.__version__,
    "python_version": platform.python_version(),
    "n_features": int(data.data.shape[1]),
    "classes": list(data.target_names),
    "cv_accuracy": round(float(cv_accuracy), 3),
}
with open("cancer_model.json", "w", encoding="utf-8") as f:
    json.dump(metadata, f, indent=2)

with open("cancer_model.json", encoding="utf-8") as f:
    saved = json.load(f)
if saved["sklearn_version"] != sklearn.__version__:
    print("Warning: model saved with a different scikit-learn version")
print(json.dumps(saved, indent=2))
Output
{
  "model_file": "cancer_model.joblib",
  "sklearn_version": "1.8.0",
  "python_version": "3.14.2",
  "n_features": 30,
  "classes": [
    "malignant",
    "benign"
  ],
  "cv_accuracy": 0.981
}

Good metadata also records where the training data came from (or a fixed snapshot of it) and which code version trained the model, so anyone can rebuild the model if needed. Tools for tracking this automatically are covered in MLOps Basics.

Common mistakes

  • Saving only the final model. Without the fitted scaler and encoders, new data cannot be prepared correctly. Save the whole pipeline.
  • Loading model files from untrusted sources. pickle and joblib files can run code; use skops.io or ONNX for shared models, and only load files you trust.
  • Upgrading scikit-learn under a saved model. Pin versions, record them in metadata, and retrain when you upgrade.
  • Saving a model that depends on functions defined in a notebook. A FunctionTransformer using your own function needs that function importable when loading; put it in a proper module.

Key points

  • Save fitted models with joblib.dump and load them with joblib.load; save the whole pipeline.
  • pickle-based files (pickle, joblib) can execute code when loaded, so never load them from untrusted sources.
  • skops.io loads only reviewed, trusted types; ONNX runs models without scikit-learn via onnxruntime.
  • Load models with the same scikit-learn version that saved them; a mismatch triggers InconsistentVersionWarning.
  • Store metadata (versions, features, scores, data source) next to every saved model.

Exercise

Train a StandardScaler + LogisticRegression pipeline on the wine dataset (test_size=0.25, random_state=42, stratify). Save it with joblib.dump(..., compress=3), load it back, and print whether the loaded model's test predictions equal the original's, the test accuracy, and the class labels it knows (classes_).

Show solution
Python
import joblib
import numpy as np
from sklearn.datasets import load_wine
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X, y = load_wine(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y)
pipe = make_pipeline(StandardScaler(), LogisticRegression()).fit(X_train, y_train)

joblib.dump(pipe, "wine_model.joblib", compress=3)
loaded = joblib.load("wine_model.joblib")
print("Same predictions:", np.array_equal(pipe.predict(X_test), loaded.predict(X_test)))
print(f"Test accuracy: {loaded.score(X_test, y_test):.3f}")
print("Classes:", loaded.classes_)
Output
Same predictions: True
Test accuracy: 1.000
Classes: [0 1 2]

A pipeline exposes classes_ from its final step, so the loaded file knows exactly which labels it can predict. Compression makes no difference to the predictions, only to the file size.

Quick Quiz

1. Why should you save the whole pipeline instead of only the final model?

2. What is the main security risk of pickle and joblib files?

3. Which format lets you run a scikit-learn model without scikit-learn installed?

4. What should you record alongside a saved model?

Learn Machine Learning with a mentor

Free lessons get you started. Our Data Science & Machine Learning training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call