Python Tutorial
Saving, Serving and Next Steps in Machine Learning
A model is only useful when it can make predictions for real users. This final lesson shows how to save a trained pipeline with joblib, load it in another program, and serve predictions from a FastAPI endpoint — combining this module with the web module. It ends with the practices that keep models trustworthy in production and a roadmap into deep learning and working with large language models.
Saving and Loading Models
joblib.dump(pipeline, "model.joblib") saves the whole fitted pipeline — preprocessing and model — and joblib.load() restores it. Save the pipeline, not only the model, so production applies exactly the same preprocessing. Record the scikit-learn version and training date alongside it: loading a model with a different library version is not guaranteed to work. Only load model files you trust — joblib/pickle files can execute code when loaded. For cross-language serving, formats such as ONNX (skl2onnx) are an alternative.
ML in Production
- Load the model once at start-up (a FastAPI lifespan handler), not on every request.
- Validate inputs with a Pydantic model that mirrors the training features.
- Log predictions and, later, the true outcomes to measure real-world accuracy.
- Watch for data drift — when incoming data stops looking like the training data — and retrain regularly.
- Check models for bias across groups of users before and after deployment.
Where to Go Next
- Gradient boosting libraries — XGBoost, LightGBM, CatBoost: top performers on tabular data.
- Deep learning — PyTorch (most popular in research and industry) or TensorFlow/Keras for images, audio, text and time series.
- Large language models — calling LLM APIs from Python, embeddings for semantic search, retrieval-augmented generation (RAG) and agents.
- MLOps — experiment tracking (MLflow), model registries, automated retraining and monitoring.
- Practice on real datasets (Kaggle, UCI Machine Learning Repository) and build end-to-end projects.
Examples
Training, saving and loading a pipeline with joblib
# train_model.py
import json
import joblib
import sklearn
from datetime import date
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
iris = load_iris(as_frame=True)
pipeline = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
pipeline.fit(iris.data, iris.target)
joblib.dump(pipeline, "iris_model.joblib")
metadata = {"sklearn_version": sklearn.__version__, "trained_on": str(date(2026, 9, 28)),
"features": list(iris.data.columns), "classes": iris.target_names.tolist()}
json.dump(metadata, open("iris_model.json", "w"), indent=2)
print("saved model with features:", metadata["features"])
# later, in another program
loaded = joblib.load("iris_model.joblib")
sample = iris.data.iloc[[0, 75, 140]]
print("predictions:", [metadata["classes"][i] for i in loaded.predict(sample)])
print("same as original:", (loaded.predict(sample) == pipeline.predict(sample)).all())
saved model with features: ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
predictions: ['setosa', 'versicolor', 'virginica']
same as original: True
Serving predictions with FastAPI
# app.py (run with: fastapi dev app.py)
from contextlib import asynccontextmanager
import joblib
import pandas as pd
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
CLASSES = ["setosa", "versicolor", "virginica"]
ml = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
ml["model"] = joblib.load("iris_model.joblib") # load once at start-up
yield
ml.clear()
app = FastAPI(title="Iris classifier", lifespan=lifespan)
class Flower(BaseModel):
sepal_length: float = Field(gt=0, le=10)
sepal_width: float = Field(gt=0, le=10)
petal_length: float = Field(gt=0, le=10)
petal_width: float = Field(gt=0, le=10)
@app.post("/predict")
def predict(flower: Flower):
row = pd.DataFrame([[flower.sepal_length, flower.sepal_width, flower.petal_length, flower.petal_width]],
columns=["sepal length (cm)", "sepal width (cm)", "petal length (cm)", "petal width (cm)"])
probabilities = ml["model"].predict_proba(row)[0]
best = int(probabilities.argmax())
return {"species": CLASSES[best], "confidence": round(float(probabilities[best]), 3)}
with TestClient(app) as client:
print(client.post("/predict", json={"sepal_length": 5.1, "sepal_width": 3.5,
"petal_length": 1.4, "petal_width": 0.2}).json())
print(client.post("/predict", json={"sepal_length": 6.7, "sepal_width": 3.0,
"petal_length": 5.2, "petal_width": 2.3}).json())
print(client.post("/predict", json={"sepal_length": -1, "sepal_width": 3.0,
"petal_length": 5.2, "petal_width": 2.3}).status_code)
{'species': 'setosa', 'confidence': 0.985}
{'species': 'virginica', 'confidence': 0.961}
422
Common Mistakes
- Saving only the model and re-implementing preprocessing differently in production.
- Loading the model inside the request handler, making every prediction slow.
- Loading joblib/pickle files from untrusted sources.
- Deploying once and never checking accuracy or data drift again.
- Ignoring library versions, so a saved model fails to load after an upgrade.
Key Points to Remember
- joblib.dump/load save and restore complete fitted pipelines.
- Store metadata: library version, features, classes and training date.
- Serve models from FastAPI: load once in lifespan, validate inputs with Pydantic.
- Monitor predictions, drift and fairness; retrain as data changes.
- Next steps: gradient boosting, PyTorch deep learning, LLM applications and MLOps.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local project environment for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.