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
Last updated: 8 October 2026
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.
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)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):
| Situation | Format |
|---|---|
| Serve predictions without Python, or in a lean environment | ONNX, if the model converts |
| Need the Python object, and the file may not be fully trusted | skops |
| Need the Python object, and the source is fully trusted | joblib or pickle |
| Exotic objects the others cannot handle | cloudpickle, 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?
What is the difference between pickle and joblib?
What is skops?
When should I convert a model to ONNX?
Model serialisation with pickle and joblib, and deployment strategies, are part of the Model Deployment module of the MLOps course.
Sources
- pickle: Python object serialization (Python documentation) — Python Software Foundation
- Model persistence (scikit-learn documentation) — scikit-learn
- Secure persistence with skops (skops documentation) — skops
- Bike Sharing dataset (UCI Machine Learning Repository) — UCI Machine Learning Repository
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.
Related articles
Monitoring ML Models in Production: A Complete Guide
A model that passed validation can fail quietly in production. This guide sets out what to monitor at each layer, how to cope with labels that arrive late and how to design alerts people act on, using a year of real sensor data in which the loudest alert was the least useful one.
Production ML Systems: Architecture and Best Practices
In a production machine learning system the model is the smallest part. This guide maps the components around it, compares the three serving patterns, explains the ML-specific technical debt that architecture has to contain, and sets out the practices that keep a system maintainable as it grows.
Scaling Model Training with Distributed Computing
Distributed training splits the work across machines, but keeping them in step costs time. In real PyTorch data-parallel runs with one, two and four workers, every replica stayed identical, yet small batches trained slower with more workers and only large batches gained, about one and a half times with four.