From a notebook to a service
Lesson 1 of 6
1From a notebook to a service
The big pictureA model running in a Jupyter notebook is not a system. A system is an endpoint any program can send a request to and get a reply, a model loaded once at startup rather than on every request, input validation before anything reaches the model, and a health check telling whoever monitors the service that it is alive and ready.
What the picture shows: The request passes validation before it touches the model, so empty text returns 422 and a broken body 400. The model in the middle was loaded when the service started, not per request, and the reply carries its version.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
A model in a notebook is like a recipe in a cook's notebook: it works only for them. For people to use it, you open a restaurant: a window taking orders, the kitchen set up once in the morning rather than per order, and an "open" sign passers-by can see.
You wrap the model in an API like FastAPI with a POST /predict endpoint and pydantic input and output schemas, loading the model once at startup, not per request. A GET /health endpoint returns service status and the model version for monitoring and load balancers.
Before You Start
📞Calling a model
You have called models through an API and know what a JSON request and reply look like.
🐍Python functions
You write a function that takes input and returns a result, and you know exceptions.
The Core Idea
A service is an endpoint any program can send a request to and get a reply. Here you build loading the model once, then the request handler, then status codes, then the readiness check with FastAPI.
1Load the model once
Loading a model takes seconds to minutes and uses a lot of memory. If it happened on every request, the service would be unusable.
Look at the picture: four requests. Loading inside the request means four loads, and loading at startup means one, shared by everyone.
2The handler: validate, then predict
The endpoint is an address like POST /predict that takes JSON and returns JSON in a fixed, documented shape. Before anything reaches the model we check type, length and required fields.
Before FastAPI, let us see what any framework does: receive text, validate it, call the preloaded model, and return a status code and a JSON reply.
import json
class SentimentModel:
loads = 0
def __init__(self):
SentimentModel.loads += 1 # in real life: seconds of reading weights from disk
self.version = "sentiment-2026-05-01"
def predict(self, text):
return "positive" if any(w in text for w in ["ممتاز", "شكرا", "رائع"]) else "negative"
MODEL = SentimentModel() # loaded once, when the service starts
def handle(body):
try:
data = json.loads(body)
except json.JSONDecodeError:
return 400, {"error": "body must be JSON"}
text = data.get("text")
if not isinstance(text, str) or not text.strip():
return 422, {"error": "field 'text' must be a non-empty string"}
if len(text) > 2000:
return 422, {"error": "text is longer than 2000 characters"}
return 200, {"label": MODEL.predict(text), "model_version": MODEL.version}
def health():
return 200, {"status": "ok", "model_version": MODEL.version}
for body in ['{"text": "خدمة ممتاز وشكرا"}', '{"text": ""}', 'not json', '{"text": "تأخر الطلب"}']:
print(handle(body))
print(health())
print("model loads after 4 requests:", SentimentModel.loads)
Four requests and the model was loaded once. And the codes tell the client what happened: 200 success, 400 the request itself is broken, 422 the request is well-formed but its content is not acceptable. Every successful reply carries the model version, so when someone complains about an answer you know which model gave it.
3Status codes
The status code tells the client what happened before it reads the reply. Look at the picture: three codes cover most cases.
400 means the request itself is broken, like invalid JSON. 422 means the request is well formed but its content is not acceptable, like empty or overlong text.
4FastAPI and the readiness check
The service is alive as soon as the server runs, but not ready until the model finishes loading. So we split two checks: /health/live and /health/ready.
Look at the picture: the deployment platform sends requests only after the readiness check answers 200.
This is what you will actually write. It needs pip install fastapi uvicorn, so it is here for reading.
from contextlib import asynccontextmanager
from fastapi import FastAPI
from pydantic import BaseModel, Field
state = {}
@asynccontextmanager
async def lifespan(app):
state["model"] = load_model("/models/sentiment-v3") # once, before the first request
yield
state.clear()
app = FastAPI(lifespan=lifespan)
class PredictIn(BaseModel):
text: str = Field(min_length=1, max_length=2000)
class PredictOut(BaseModel):
label: str
model_version: str
@app.post("/predict", response_model=PredictOut)
def predict(body: PredictIn):
model = state["model"]
return PredictOut(label=model.predict(body.text), model_version=model.version)
@app.get("/health/ready")
def ready():
return {"status": "ok", "model_version": state["model"].version}
lifespan loads the model before the first request, Pydantic validates input and returns 422 with error details automatically, and interactive docs appear at /docs with no extra line. Run it with uvicorn app:app and try it from the browser.
How It Works, Step by Step: how the service handles a request
At startup the model loads once, and /health/ready returns 200 only after loading completes.
POST /predict arrives with a JSON body, and its type, length and required fields are checked.
A broken body returns 400, and unacceptable content returns 422 with a message saying what is wrong.
If the checks pass, the loaded model predicts and the reply returns in a fixed shape with the result and the model version.
Going deeper
For large language models, do not write the service from scratch. Dedicated servers like vLLM handle loading, smart request batching and context memory management, and expose an OpenAI-compatible API directly. FastAPI suits your small and medium models, and the business-logic layer around a language model running in a dedicated server.
Worked example
The first mistake in nearly every model service: loading the model inside the request function. Loading a model takes seconds to minutes, so if it happens on every request the service is unusable. The right way is to load it once when the service starts and keep it in memory.
async def function calling a heavy synchronous model. This freezes the whole event loop during prediction, so no other request is served, not even the health check. Use a plain def for synchronous compute code, and FastAPI runs it in a separate thread.
Try it without code
Myth vs Reality
| Myth | Reality |
|---|---|
| The service is ready as soon as the server runs. | Alive yes, but not ready until the model finishes loading, which can take a minute. That is why there are two checks: live and ready. |
async def is always faster in FastAPI. | With a heavy synchronous model it freezes the event loop so no other request is served. Use a plain def for synchronous compute. |
| A bad input deserves a 500. | 500 means a server error. A problem in the request itself returns 400 or 422, so the client knows to fix its input. |
Try it yourself
The service is very slow: every request takes 8 seconds, and the model itself predicts in 50 ms. What is the likely cause?
The huge gap between prediction time and request time means fixed work on every request, and loading is the classic example. Move it to service start.
Which status code do you return for a valid JSON request whose text field is empty?
500 means a server error, and the problem here is the request. 422 tells the client to fix its input, with a message saying what is wrong.
Quick review cards
POST /predict that takes JSON and returns JSON, in a fixed, documented shape any client can rely on.GET /health returns 200 when the service is truly ready and the model loaded. Deployment platforms use it to know when to send traffic and when to restart.Check Your Understanding
Why does every reply carry the model version?
Because you will deploy new versions, and two may run side by side during a rollout. When a user complains about an answer, or you see a metric drop, you need to know which model gave it. Without the version in the reply and the log, diagnosis is guesswork.
Why set a maximum text length, such as 2000 characters?
Because a hundred-thousand-character text can eat the server's memory or tie up the model for minutes for everyone. A limit with a clear message protects the service and all its users.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs1
Done with this lesson?
Sign in to save your progress.