BitWithBite
/
Tier 2 › Module 1 › Lesson 1.4
← Prev
Module 1 — The scikit-learn Workflow

Saving and
Loading Models

20 min
📚 Code + Production
🎯 Lesson 1.4 of 4
A trained model that lives only in your Jupyter notebook is useless to anyone else. This lesson covers how to persist a fitted pipeline to disk, reload it anywhere, and avoid the version mismatches that silently break production systems.

pickle vs joblib

Python has a built-in serialization module, pickle, that can save almost any Python object to disk as bytes. scikit-learn also ships a tool built specifically for this: joblib. Both work — but they're not equally good for ML.

picklejoblib
Built into PythonYes (standard library)No — separate package, but ships with sklearn
Speed with NumPy arraysSlowerMuch faster — optimized for large arrays
File size for big modelsLargerSmaller — efficient binary storage for arrays
Compression supportManualBuilt-in (compress=3 parameter)
sklearn's own recommendation✅ joblib is the official recommendation

scikit-learn models internally store their learned parameters as NumPy arrays — tree splits, coefficients, cluster centers. joblib is specifically optimized for serializing large NumPy arrays efficiently, which is exactly what these are. That's why it wins for ML.

python — pickle
import pickle

# Save
with open('model.pkl', 'wb') as f:
    pickle.dump(trained_pipeline, f)

# Load
with open('model.pkl', 'rb') as f:
    loaded_pipeline = pickle.load(f)
python — joblib (recommended for sklearn)
import joblib

# Save — one line, no context manager needed
joblib.dump(trained_pipeline, 'model_pipeline.joblib')

# Load — one line
loaded_pipeline = joblib.load('model_pipeline.joblib')

# Optional: compress to save disk space
joblib.dump(trained_pipeline, 'model_pipeline.joblib', compress=3)
Rule of thumb
Use joblib for any object containing scikit-learn models or NumPy arrays. Use pickle only when you need to serialize something joblib doesn't handle well, or you're outside the sklearn ecosystem entirely.

Always Save the Whole Pipeline, Not Just the Model

This was emphasized in lesson 1.3, but it's worth repeating because it's the #1 mistake in production ML code:

python — ❌ WRONG
# Saving ONLY the model, not the preprocessing
joblib.dump(model, 'model_only.joblib')

# Now, in production, someone has to remember:
# - which columns were scaled
# - what encoder was used for categoricals
# - what the imputation strategy was
# - the exact column order the model expects
# One mismatch = silent wrong predictions, or a crash
python — ✅ CORRECT
# Save the ENTIRE fitted pipeline
joblib.dump(full_pipeline, 'full_pipeline.joblib')

# In production: load once, then call directly on raw data
pipeline = joblib.load('full_pipeline.joblib')
prediction = pipeline.predict(raw_dataframe)  # Done.

The Version Mismatch Problem

scikit-learn's internal model format can change slightly between versions. If you save a model with scikit-learn 1.3 and load it with 1.5, you might see a warning — or in rare cases, a crash, or silently wrong behavior.

text — typical warning
InconsistentVersionWarning: Trying to unpickle estimator
RandomForestClassifier from version 1.3.0 when using version 1.5.2.
This might lead to breaking code or invalid results.
How to protect against this in production
Pin your scikit-learn version exactly in requirements.txt (scikit-learn==1.5.2, not scikit-learn>=1.5). Train and serve with the same version. If you must upgrade, retrain and re-save the model — don't just load an old pickle into a new environment and hope.

What Actually Gets Saved

When you pickle/joblib-dump a fitted pipeline, you're saving:

Contents of a saved pipeline
→ All learned parameters: coef_, mean_, tree structures, cluster centers — everything with a trailing underscore
→ All configuration: hyperparameters you set (n_estimators=100, etc.)
→ The exact structure of the pipeline — which steps, in which order
→ Column names/order expected (if you used a DataFrame and ColumnTransformer)

NOT saved: your training data itself. Only what the model learned from it.

A Minimal Production Loading Pattern

Here's how this typically looks inside a FastAPI app — load once at startup, reuse for every request:

python — FastAPI pattern
from fastapi import FastAPI
import joblib
import pandas as pd

app = FastAPI()

# Load ONCE at startup — not on every request
pipeline = joblib.load('full_pipeline.joblib')

@app.post("/predict")
def predict(data: dict):
    df = pd.DataFrame([data])
    prediction = pipeline.predict(df)[0]
    probability = pipeline.predict_proba(df)[0].max()
    return {"prediction": int(prediction), "confidence": float(probability)}
Why load at startup, not per-request
Deserializing a model from disk takes time — milliseconds to seconds depending on size. Loading it once when the server starts (not inside the endpoint function) means every request reuses the already-loaded object in memory. This is the difference between a fast API and a slow one.
✓ Check your understanding
1. Why does scikit-learn officially recommend joblib over pickle for saving models?
Apickle doesn't work with scikit-learn objects at all
Bjoblib is faster and more space-efficient for the large NumPy arrays inside fitted models
Cjoblib is part of the Python standard library and pickle isn't
Dpickle can only save one model at a time
Correct. scikit-learn models store learned parameters as NumPy arrays (coefficients, tree structures, cluster centers). joblib is specifically optimized to serialize large NumPy arrays efficiently — faster and smaller files than pickle for this exact use case.
2. You load a model saved with scikit-learn 1.3 into an environment running scikit-learn 1.6. What's the safest approach?
AIgnore any warnings — version differences never matter
BPin the exact scikit-learn version in requirements.txt to match training, or retrain and re-save with the new version
CConvert the model to JSON format first
DAlways use the newest scikit-learn version available, regardless of what trained the model
Correct. Version mismatches between training and serving environments can cause warnings, crashes, or — worst of all — silently incorrect predictions. The safe approach is pinning exact versions so train and serve environments match, or retraining the model when you do need to upgrade.
3. In a FastAPI app, where should joblib.load() be called?
AInside the endpoint function, so it's always fresh
BOnce, at module/startup level, so it's loaded once and reused for every request
CIt doesn't matter — performance is identical either way
DInside a background task that runs every hour
Correct. Loading the model from disk takes real time (milliseconds to seconds). Calling joblib.load() inside the endpoint function would mean reloading the model on every single API request — extremely slow. Loading once at startup means it's already in memory, ready to use instantly for every prediction.

Summary

What you learned
joblib beats pickle for sklearn objects — faster, smaller, official recommendation
→ Always save the entire fitted pipeline, never just the model alone
→ Version mismatches between train and serve environments can break or silently corrupt predictions — pin versions exactly
→ Load the model once at startup, never inside a per-request function
→ A saved pipeline contains learned parameters + configuration + structure — not the original training data

✓ Module 1 complete — The scikit-learn Workflow

You now understand estimators, transformers, full pipelines with ColumnTransformer, and how to persist models for production. Next: Module 2 — Decision Trees & Ensembles.

← 1.3 Building a Full Pipeline Object Back to Module 1 overview →
🗒 Cheat Sheet 📝 Worksheet