Model Packaging: From Pickle to Production

A trained model has to leave the notebook as a file, and the format decides how safe, portable and fast it is. This guide shows why loading a pickle can run arbitrary code, then saves one model as pickle, joblib, skops and ONNX and compares their size, their predictions and what each needs to load.

GloryTecks Team

Training & Placement Faculty

6 May 2026 6 min read

Last updated: 8 October 2026

Share:

Training ends with a model in memory. Production begins with a file. Between the two sits a decision most teams make by habit, usually pickle.dump, and that decision affects security, portability and how heavy the serving system has to be.

This guide shows the risk in the habit, then compares four formats on the same model.

The problem with pickle

Python's pickle format can rebuild almost any object, which is why it is popular. The price is that a pickle file can instruct the loader to call functions. The Python documentation is blunt: the module is not secure, and you should never unpickle data that could come from an untrusted source or could have been tampered with (Python: pickle).

The script's first lines make that concrete with a harmless example: an object whose pickled form tells the loader to call print. Loading it runs the call. A malicious file could call anything else instead.

Four formats, one model

The script then trains one random forest on the UCI Bike Sharing data, released under CC BY 4.0 (UCI: Bike Sharing), saves it four ways, loads each file back and compares.

python
import os
import pickle

import joblib
import numpy as np
import onnxruntime as ort
import pandas as pd
import skops.io as sio
from skl2onnx import to_onnx
from sklearn.ensemble import RandomForestRegressor


# 1. Why pickle files are dangerous: loading one can run any code its author chose
class Surprise:
    def __reduce__(self):
        return (print, ("this line was run by pickle.loads, just by loading the file",))


pickle.loads(pickle.dumps(Surprise()))

# 2. One trained model, saved four ways
day = pd.read_csv("data/day.csv")                                # UCI Bike Sharing (CC BY 4.0)
FEATURES = ["season", "yr", "mnth", "holiday", "weekday", "workingday", "weathersit", "temp", "atemp", "hum", "windspeed"]
X = day[FEATURES].astype("float32")
model = RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42).fit(X, day.cnt)
reference = model.predict(X)

with open("model.pkl", "wb") as f:
    pickle.dump(model, f)
joblib.dump(model, "model.joblib")
sio.dump(model, "model.skops")
with open("model.onnx", "wb") as f:
    f.write(to_onnx(model, X.to_numpy()[:1]).SerializeToString())

# 3. Load each one back and check the predictions
print(f"\n{'format':<8} {'size (KB)':>10} {'largest difference from the original':>38}")
loaded = {
    "pickle": pickle.load(open("model.pkl", "rb")).predict(X),
    "joblib": joblib.load("model.joblib").predict(X),
}
untrusted = sio.get_untrusted_types(file="model.skops")       # skops lists what it will not load blindly
REVIEWED = ["sklearn.tree._tree.Tree"]                          # types we checked and accept, for a file we made
assert set(untrusted) <= set(REVIEWED), f"unexpected types in the file: {untrusted}"
loaded["skops"] = sio.load("model.skops", trusted=REVIEWED).predict(X)
session = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
loaded["onnx"] = session.run(None, {session.get_inputs()[0].name: X.to_numpy()})[0].ravel()

files = {"pickle": "model.pkl", "joblib": "model.joblib", "skops": "model.skops", "onnx": "model.onnx"}
for name, predictions in loaded.items():
    size = os.path.getsize(files[name]) / 1024
    print(f"{name:<8} {size:>10,.0f} {np.abs(predictions - reference).max():>38.4f}")
print("\nskops asked before loading these types:", untrusted)
text
this line was run by pickle.loads, just by loading the file

format    size (KB)   largest difference from the original
pickle        3,571                                 0.0000
joblib        3,577                                 0.0000
skops         5,057                                 0.0000
onnx          1,890                                 0.0020

skops asked before loading these types: ['sklearn.tree._tree.Tree']

What the output shows

  • The pickle ran code on load. The sentence was printed by pickle.loads, not by any line that called the object.
  • Pickle, joblib and skops reproduced the model exactly. Their predictions matched the original to the last digit. Joblib is pickle with conveniences for large arrays, so it shares pickle's risk.
  • Skops refused to trust a type blindly. Before loading, it listed the tree structure inside the forest as a type to review. The script loads only types on an explicit reviewed list, and fails if the file contains anything else. The skops documentation describes this model: objects are loaded only if they are trusted by default or by the user (skops: secure persistence).
  • ONNX was the smallest file, 1,890 KB against about 3,571 KB for pickle, and its predictions differed by at most 0.002 rentals, a rounding effect of 32-bit arithmetic.
  • Skops files were the largest here. Safety and transparency cost some space.

Choosing a format

scikit-learn's guide to persistence frames the choice as a short series of questions (scikit-learn: model persistence):

SituationFormat
Serve predictions without Python, or in a lean environmentONNX, if the model converts
Need the Python object, and the file may not be fully trustedskops
Need the Python object, and the source is fully trustedjoblib or pickle
Exotic objects the others cannot handlecloudpickle, with the same cautions as pickle

The guide adds two constraints worth repeating. ONNX does not support every model. And pickle, joblib and skops all need an environment with the same packages and versions as the one that trained the model. None of them supports loading a model trained with a different version of scikit-learn.

Verify before you load

Whatever the format, the serving system should refuse files it cannot vouch for:

  • Load only from your own artifact store or registry, never from a path a user or an upstream job can write to freely.
  • Check a checksum recorded when the model was approved, and stop if the file differs.
  • Run a smoke test at start-up: load the model, predict on a few stored inputs, and compare with stored outputs.

Check conversions, every time

A converted model is a new artefact, so it needs its own check. After converting to ONNX, run both versions on a sample of real inputs and compare, as the script does, and set a tolerance in advance. Here the largest difference was 0.002 rentals, far below anything that matters. A larger gap usually means an unsupported option or a type mismatch, and the converted model should not ship.

What goes into a deployable package

A model file alone is not a package. Ship it with:

  • The exact dependency versions, in a lock file or container image. See Docker for machine learning.
  • The input schema: feature names, types and order.
  • The preprocessing, ideally inside the saved pipeline rather than reimplemented by the serving team.
  • Metadata: model version, training data version and evaluation results, usually from the registry. See the model registry.
  • A checksum, so the serving system can confirm the file is the one that was approved.

For putting the package behind an API, see serving ML models with FastAPI and Docker.

Common mistakes

  • Loading pickles from downloads, emails or shared drives without knowing their origin.
  • Trusting every type skops reports just to make a file load.
  • Shipping a model without its exact dependency versions.
  • Reimplementing preprocessing in the serving code instead of saving the pipeline.
  • Assuming ONNX supports every model without converting and checking the predictions.

Is it safe to load a pickle file?

Only if you fully trust its source and it cannot have been tampered with. Loading a pickle can execute arbitrary code, as the Python documentation warns and the example above demonstrates.

What is the difference between pickle and joblib?

Joblib builds on pickle and handles large NumPy arrays more efficiently. It has the same security risk, because loading a joblib file also unpickles objects.

What is skops?

A library for saving scikit-learn models more securely than pickle. It records the types in a file and loads only those that are trusted by default or explicitly approved by the user.

When should I convert a model to ONNX?

When predictions must run without Python, in a lean or non-Python environment, or across frameworks. Check that your model type is supported and compare predictions after conversion, since small numerical differences can appear.

Model serialisation with pickle and joblib, and deployment strategies, are part of the Model Deployment module of the MLOps course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master MLOps?

Join GloryTecks’ expert-led MLOps training in Hyderabad with real-time projects and 100% placement support.

Explore course