Saving and Loading Models (Pickle, Joblib)
Training a model can take minutes, hours, or days. You don’t want to redo that work
every time you restart a script or every time a new API request comes in. The fix is
simple: train once, write the trained object to disk as a file, and load that file
back whenever you need predictions. This page covers how to do that safely with
joblibjoblib and picklepickle, and the mistakes that trip people up in production.
Why we save models
Training can be expensive.
Instead of training every time, we:
- train once
- save model artifact
- load it for predictions
joblib vs pickle
picklepickle: general Python serializationjoblibjoblib: often better for large numpy arrays (common in sklearn models)
For scikit-learn, joblibjoblib is a common default.
Best practice: save the pipeline
Don’t save only the model.
Save the entire pipeline (preprocessing + model), so inference matches training.
If you save just the fitted LogisticRegressionLogisticRegression and forget the StandardScalerStandardScaler or
OneHotEncoderOneHotEncoder that fed it, the numbers you feed at prediction time won’t match the
numbers the model actually learned from — the model will run, but its predictions
will be wrong in a way that’s hard to notice.
The save → version → load flow
flowchart LR
T["Train pipeline"] --> S["joblib.dump\(model, 'model_v1.joblib'\)"]
S --> R["Registry / disk / cloud bucket"]
R --> L["joblib.load\('model_v1.joblib'\)"]
L --> P["pipeline.predict\(X_new\)"]
Saving and loading a real pipeline
import joblib
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# tiny synthetic dataset: [age, income] -> subscribed (0/1)
X = np.array([
[22, 20000], [35, 50000], [45, 80000],
[23, 22000], [40, 60000], [55, 95000],
])
y = np.array([0, 1, 1, 0, 1, 1])
pipeline = Pipeline([
("scaler", StandardScaler()),
("clf", LogisticRegression()),
])
pipeline.fit(X, y)
# save the WHOLE pipeline, not just the classifier
joblib.dump(pipeline, "model_v1.joblib")
print("Saved model_v1.joblib")import joblib
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# tiny synthetic dataset: [age, income] -> subscribed (0/1)
X = np.array([
[22, 20000], [35, 50000], [45, 80000],
[23, 22000], [40, 60000], [55, 95000],
])
y = np.array([0, 1, 1, 0, 1, 1])
pipeline = Pipeline([
("scaler", StandardScaler()),
("clf", LogisticRegression()),
])
pipeline.fit(X, y)
# save the WHOLE pipeline, not just the classifier
joblib.dump(pipeline, "model_v1.joblib")
print("Saved model_v1.joblib")import joblib
import numpy as np
pipeline = joblib.load("model_v1.joblib")
X_new = np.array([[30, 45000], [50, 90000]])
preds = pipeline.predict(X_new)
print("Predictions:", preds)import joblib
import numpy as np
pipeline = joblib.load("model_v1.joblib")
X_new = np.array([[30, 45000], [50, 90000]])
preds = pipeline.predict(X_new)
print("Predictions:", preds)Versioning artifacts (so rollback is trivial)
A simple, effective pattern is to bake a version number or timestamp into the
filename instead of always overwriting model.joblibmodel.joblib:
import joblib
from datetime import datetime
def save_versioned(pipeline, name="model"):
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
path = f"{name}_{stamp}.joblib"
joblib.dump(pipeline, path)
return path
# path = save_versioned(pipeline)
# -> "model_20260712_143000.joblib"import joblib
from datetime import datetime
def save_versioned(pipeline, name="model"):
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
path = f"{name}_{stamp}.joblib"
joblib.dump(pipeline, path)
return path
# path = save_versioned(pipeline)
# -> "model_20260712_143000.joblib"If a new version misbehaves in production, rolling back is just pointing your loading code at the previous file — no retraining required.
Common pitfalls
- version mismatches (sklearn/numpy versions)
- saving raw model but forgetting preprocessing
- loading untrusted pickle files
Security note
Never unpickle files from untrusted sources.
Pickle can execute arbitrary code.
🧪 Try It Yourself
Exercise 1 – Save a Fitted Model with joblib
Exercise 2 – Load a Model and Predict
Exercise 3 – Build a Versioned Filename
Next
Continue to Building an ML API with Flask/FastAPI — now that the pipeline can be saved and loaded reliably, wrap it in an HTTP endpoint so other services can call it.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
