5 · Loading the model, validating input, handling errors
A production-grade route loads the `.joblib` model ONCE at startup (not per request), validates the payload before predicting (keys present, numeric, in range), and wraps the work in `try/except` to return structured errors with the right status codes — never trusting client input.
A robust route does three things the naive one skips: it loads the `.joblib` model ONCE at module level (not per request, which is slow), it VALIDATES the payload BEFORE predicting (keys present, numeric, finite, in range), and it wraps work in `try/except` to return STRUCTURED errors with correct status codes (`422` bad input, `500` server fault) — because you never trust client input.
Without this:
Loading the model per request makes every call slow; skipping validation lets malformed input crash the worker or — worse — return a confident, meaningless prediction; and unhandled exceptions leak stack traces and return wrong status codes.
Lessons 3 and 4 got the route working. This lesson makes it robust — the difference between a demo and something you'd put in front of real traffic. Three disciplines:
1. Load the model ONCE, at startup. Put model = joblib.load(...) at module level — it runs a single time when the app boots. Do not load it inside the route, or you'll re-read and deserialize the file on every single request, making each prediction needlessly slow and wasting memory. Loading is slow; requests must be fast; so you pay the loading cost once and reuse the in-memory model forever after. (As a bonus, if the model file is missing, the app fails loudly at startup instead of on the first user's request.)
2. Validate the payload BEFORE predicting. This is the heart of the lesson. model.predict has a strict contract — a fixed set of features, all numeric, finite, within a sane range — and it knows nothing about enforcing it. So you enforce it, with a validation gate that runs first and checks, in order:
- Keys present — every required feature is in the payload (else
422, name the missing one). - Numeric — each value coerces to
float(else422, a string like"high"is rejected). - Finite — no
NaN/inf(these sneak past type checks but poison the model). - In range — within the bounds you expect (a
tenureof 999 years is almost certainly a bug).
Only if all checks pass do you call model.predict. A bad request becomes a clear 422 with a message saying exactly what's wrong — not a cryptic crash, and never a silent wrong answer.
3. Handle errors with try/except and the right status code. Wrap the prediction in try/except. A validation failure is the client's fault → return 422 (or 400) with a structured JSON error like {"error": "..."}. An unexpected failure is the server's fault → return 500, log the real traceback server-side, but send the client a generic message (never leak a stack trace — it's noise to them and a security risk to you). Every response is intentional: a result, or a structured error with the correct code.
Why validate on the SERVER even when the form already validates on the client? Because client-side validation is a convenience, not a guarantee. HTML required/type="number" attributes give the user instant feedback, but they're trivially bypassed — anyone can disable JavaScript, edit the HTML, or skip the form entirely and curl your endpoint with whatever bytes they like. The server is the only place you actually control, so it's the only place validation is enforceable. Client validation improves UX; server validation is the real security boundary.
The read-along cell shows the hardened route. The runnable cell below is the validation gate itself — rejecting missing-key, non-numeric, NaN, and out-of-range payloads with clear errors, accepting a valid one, then predicting — on a tiny trained model.
A hardened Flask `/predict` route: model loaded once at module level, a validation gate (keys present, numeric, finite, in range) before predicting, and `try/except` returning `422` for bad input and `500` (with a server-side log) for unexpected failures.
Python (in browser)
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Your HTML form already marks every field as `required` and `type="number"`. Why must the Flask route ALSO validate the input on the server?
- Load the `.joblib` model ONCE at module level so it boots a single time and is reused — never inside the route, which would re-load it on every request.
- Validate the payload BEFORE predicting — keys present, numeric, finite, in range — and return a clear `422` naming exactly what's wrong on failure.
- Never trust client input: client-side validation is UX only and easily bypassed, so the server-side gate is the real security boundary — return `422` for bad input, `500` (with a server log) for genuine failures.
Every production model service loads its artifact once at startup and gates every request through input validation — it's the standard shape that keeps a real-time predictor fast and trustworthy.
If you remove it: Per-request loading makes the service slow, and an unvalidated route either crashes on malformed input or returns confident garbage — both unacceptable when a prediction drives a real decision.