Serving & API avec FastAPI — donner une adresse au cerveau, pour que n'importe quelle application puisse lui demander un verdict.
Le fil rouge · où on en est
Le cerveau existe — et personne ne peut lui parler
Bloc 2 : vous avez entraîné un détecteur de fraude, évalué honnêtement, seuil justifié en euros. Mais regardez la carte : le cerveau vit dans un cadre en pointillés — un notebook, sur un laptop. L'app de paiement, les analystes, les serveurs de la banque : personne ne peut l'interroger.
Mission du bloc 3 : donner une adresse au modèle — une API que toute la banque peut appeler.
FastAPIPydanticpytest
L'objectif du bloc
À la fin du bloc, n'importe quelle application de la banque peut demander un verdict — en temps réel
Un endpoint POST /predict qui répond en quelques millisecondes, des entrées validées, des tests qui prouvent que ça marche, une doc qui ne ment jamais. Le modèle devient un service.
Bloc 3 · 3 h · théo 0h30 / prat 2h30 · jours 1–2
Une théorie courte, puis au clavier
0h30
Stratégies de serving (batch vs temps réel) & training-serving skewLes deux façons de servir un modèle, et l'erreur invisible qui guette entre entraînement et serving.
théorie
1h20
API FastAPI : endpoint /predict, modèle chargé au démarrageLe premier guichet — une transaction entre, un verdict sort, en quelques millisecondes.
pratique
0h50
Validation Pydantic + endpoints /batch, /feedback, /healthBlinder la porte, puis ouvrir les autres guichets — dont la boucle du verdict humain.
pratique
0h20
Tests pytest & documentation SwaggerProuver que le contrat tient, et le documenter sans écrire une ligne de doc.
pratique
À cheval sur deux jours : ≈ 2 h le jour 1 (théorie + /predict), ≈ 1 h le jour 2 (validation, guichets, tests).
Acte 1 · théorie 0h30
Un cerveau muet ne sert à rien
Il est 3 h 47. Une carte française est utilisée à Tallinn. L'app de la banque a 300 millisecondes pour décider — et votre modèle dort dans un notebook, sur un laptop éteint.
Le meilleur modèle du monde, s'il est injoignable au moment où on a besoin de lui, vaut exactement zéro.
Le mot du jour
« Servir » un modèle
Servir, c'est rendre les prédictions accessibles à ceux qui en ont besoin — au moment où ils en ont besoin.
Le data scientist ne sera pas là à 3 h 47, et personne ne relancera un notebook. Le serving, c'est faire répondre le modèle sans humain dans la boucle : une machine demande, une machine répond. Toute la question est quand et comment.
servinginferenceprediction service
Les deux façons de servir
Batch ou temps réel : deux métiers différents
Batch · scorer la nuit
Un gros calcul planifié
Toutes les transactions du jour, scorées à 2 h du matin, résultats déposés dans une table
Latence : on attend le prochain passage — des heures
Simple, robuste, économe — quand la décision peut attendre demain
Temps réel · répondre à la demande
Une API toujours debout
Une transaction arrive, un verdict repart — pendant le paiement
Latence : des millisecondes, exigées par le métier
Plus exigeant : disponibilité, validation, monitoring — tout ce bloc
Le choix
Le bon mode, c'est le métier qui le dicte
Batch suffit quand…
…la décision peut attendre : relances marketing, rapports de risque, scores de churn recalculés chaque nuit.
des heures de latence, et tout le monde est content
Temps réel obligatoire quand…
…la décision est en cours : le client attend devant le terminal, la fraude se joue maintenant ou jamais.
300 ms pour dire oui ou non
Notre détecteur sera temps réel — et beaucoup d'équipes font les deux : temps réel pour bloquer, batch pour analyser. Notre API saura faire les deux aussi.
L'ennemi invisible · training-serving skew
Entraîné sur un monde, servi dans un autre
Le skew, c'est quand les features ne sont pas calculées pareil à l'entraînement et au serving. Le modèle reçoit des chiffres qui ressemblent aux bons — et se trompe en silence : pas de crash, pas d'erreur dans les logs, juste des verdicts faux.
L'API répond 200 OK et le modèle raconte n'importe quoi. C'est le pire bug du ML : celui qu'on ne voit pas.
# à l'entraînement (bloc 2)
df["amount_log"] = np.log1p(df["Amount"])
# au serving, écrit six mois plus tard…
features = [req.amount, req.hour, ...] # ← montant brut : skew !
Deux lignes écrites par deux personnes, à six mois d'écart. Aucune ne crashe.
S'en protéger
Quatre parades contre le skew
Parade 1
Une seule source de vérité
La fonction de features vit dans src/, importée par le train ET par l'API. Jamais deux copies du même calcul.
Parade 2
Embarquer le préprocesseur
Le scaler est sauvé avec le modèle, en pipeline : ce qui a appris ensemble est servi ensemble.
Parade 3
Tester le pipeline complet
Un test envoie une transaction brute à l'API et vérifie le score exact attendu. On l'écrit à l'acte 4.
Parade 4
Monitorer les distributions
Comparer ce qui entre en prod à ce qui a servi à l'entraînement. C'est le sujet du bloc 7 — patience.
Acte 2
FastAPI, la porte d'entrée
Avant la techno · l'idée
Une API est un contrat
« Envoie-moi une transaction sous cette forme, je te renvoie un verdict sous cette forme. » Rien de plus, rien de moins.
L'app de paiement ne sait pas que c'est du scikit-learn derrière — et n'a pas besoin de le savoir. Tant que le contrat est tenu, on peut changer le modèle, le langage, la machine : personne ne s'en aperçoit. Ce découplage est ce qui rend le système évolutif.
Ce qu'on gagne
Trois choses qu'on gagne d'un coup
Le découplage
Le modèle évolue sans toucher aux applications ; les applications évoluent sans toucher au modèle. Chacun sa vie, le contrat entre les deux.
Le point d'entrée unique
Une seule porte à surveiller, sécuriser, mesurer. Tout ce qui entre et sort du modèle passe par là — un luxe pour le MLOps.
Le langage universel
HTTP + JSON : l'app mobile, le site web, le vieux système de la banque — tout le monde parle ça, depuis n'importe quel langage.
C'est le même geste qu'un restaurant qui ouvre une salle : la cuisine reste privée, seul le menu est public.
Feynman · 3 minutes
Un réceptionniste et ses guichets
Un serveur web, c'est un réceptionniste : il attend qu'on lui parle. Chaque route est un guichet — POST /predict, le guichet « verdict » ; GET /health, le guichet « ça va ? ». Le client dépose un formulaire (le JSON), le réceptionniste le porte au cerveau, et rapporte la réponse.
FastAPI, c'est l'art de déclarer des guichets en trois lignes de Python.
Le client ne rencontre jamais le cerveau — seulement les guichets.
Le vocabulaire minimal
HTTP en trois idées — tout ce qu'il faut aujourd'hui
Un verbe + une route
POST /predict = « je t'envoie des données, décide ». GET /health = « donne-moi une info ». Le verbe dit l'intention, la route dit le guichet.
Du JSON dans les deux sens
Des clés, des valeurs, rien d'exotique. La lingua franca des machines — lisible aussi par les humains, ce qui sauve des soirées de debug.
Un code de statut
200 = tout va bien. 422 = ta requête est invalide. 500 = j'ai un problème. Trois nombres suffisent pour toute la journée.
POSTGETJSON200422500
La techno choisie
Pourquoi FastAPI — et pas Flask ou Django
Les types travaillent
On déclare amount: float — et FastAPI valide, convertit, documente. Le typage Python cesse d'être décoratif.
La doc s'écrit seule
Swagger généré depuis le code, toujours à jour, interactif. On la découvre à l'acte 4 — elle est déjà en train de s'écrire.
Rapide par conception
Async natif, l'un des frameworks Python les plus rapides. Largement de quoi tenir nos 300 millisecondes.
Devenu le standard de facto pour servir des modèles ML en Python — la question d'entretien n'est plus « pourquoi FastAPI ? » mais « pourquoi pas ? ».
Le premier guichet
/predict tient en dix lignes
# src/api/main.py
from fastapi import FastAPI
app = FastAPI(title="Fraud Detector")
Le décorateur déclare le guichet, la fonction fait le travail. Et regardez le seuil 0,31 : c'est celui que vous avez justifié en euros au bloc 2 — la décision métier voyage avec le code.
Ce couple requête/réponse est le contrat. Tout le reste du bloc — validation, tests, documentation — ne sert qu'à une chose : le rendre incassable.
Histoire vraie · le bug du premier jour
2 secondes ou 8 millisecondes
Le modèle rechargé à chaque requête
joblib.load() écrit dans la fonction predict : le modèle entier relu depuis le disque à chaque transaction. Ça marche — lentement.
~2 s par requête. Le paiement a expiré.
Le modèle chargé au démarrage
Chargé une fois quand l'API démarre, gardé en mémoire, réutilisé par toutes les requêtes suivantes.
~8 ms par requête — 250× plus vite
Même modèle, même machine : seule la place d'une ligne de code change. C'est LE réflexe serving de la journée.
Le réflexe, en code
Charger une fois, répondre mille fois
# exécuté UNE fois, au démarrage de l'API
@asynccontextmanager
async def lifespan(app: FastAPI):
ml["model"] = joblib.load("models/model.pkl") # ~2 s, une seule fois
yield # ← l'API vit ici, le modèle déjà en mémoire
ml.clear() # à l'extinction
app = FastAPI(lifespan=lifespan)
Les 2 secondes de chargement sont payées une seule fois, au démarrage. Ensuite, chaque requête trouve le modèle déjà là.
Async · la version simple
Le réceptionniste ne reste pas au téléphone
Quand une base de données met 80 ms à répondre, un serveur classique attend, bras croisés. Un serveur async passe au client suivant et revient quand la réponse arrive. Même réceptionniste, zéro temps mort.
Pour nous, aujourd'hui : def suffit. Retenez juste pourquoi async existe — et que FastAPI le maîtrise nativement.
Async ne rend rien plus rapide — il recycle l'attente.
Démo live · à vous de jouer
Interrogez le modèle
en attente d'une requête…
Requête → POST /predict
# cliquez sur un bouton pour envoyer
Réponse
# la réponse s'affichera ici
Acte 3
Blinder la porte
Premier jour en production : une transaction arrive sans montant. La deuxième affiche −50 €. La troisième dit « mille euros » — en toutes lettres.
Le monde extérieur est créatif. Une API qui gobe tout transmet le chaos directement au modèle : garbage in, garbage out — avec un code 200 pour faire joli.
Pydantic · le videur
Personne n'entre sans papiers valides
Pydantic vérifie chaque requête à la porte : les champs attendus, du bon type, dans les bonnes bornes. Ce qui passe est propre — garanti. Ce qui ne passe pas est refusé avant même de toucher le modèle.
On déclare le contrat une fois, en Python typé. Le videur, lui, ne dort jamais.
class Transaction(BaseModel):
amount: float = Field(gt=0, le=100_000)
hour: int = Field(ge=0, le=23)
v14: float
v12: float # champ manquant ou invalide → refusé
La classe EST le contrat — lisible par un humain et exécutée par la machine.
Le refus automatique
422 : le refus poli, précis, gratuit
Requête invalide ? FastAPI répond tout seul — sans une ligne de code de votre part — et désigne exactement le champ fautif.
# amount: -50 → HTTP 422 Unprocessable Entity
{ "detail": [{
"loc": ["body", "amount"],
"msg": "Input should be greater than 0",
"type": "greater_than" }] }
Le développeur de l'app de paiement sait immédiatement quoi corriger — sans vous appeler à 3 h du matin. Une bonne erreur est une documentation.
Guichet 2 · /batch
Scorer 10 000 transactions d'un coup
Les analystes veulent re-scorer l'historique de la nuit. 10 000 appels à /predict ? Non — un appel, une liste.
@app.post("/batch")
def batch(txns: list[Transaction]):
X = to_features(txns)
scores = model.predict_proba(X)[:, 1] # vectorisé : un seul passage
return {"scores": scores.round(4).tolist(), "count": len(txns)}
NumPy score 10 000 lignes presque aussi vite qu'une seule — le serving batch et le temps réel de l'acte 1 cohabitent dans la même API.
Guichet 3 · /feedback · la boucle
Le retour de la vérité terrain
L'analyste examine l'alerte et rend le verdict humain : vraie fraude ou fausse alerte. /feedback enregistre cette réponse. C'est la donnée la plus précieuse du système — celle qui, demain, ré-entraînera le modèle.
Souvenez-vous de la carte système : cette flèche retour transforme un modèle figé en système qui apprend. On la referme complètement au bloc 8.
class Feedback(BaseModel):
transaction_id: str
true_label: int = Field(ge=0, le=1)# le verdict humain
@app.post("/feedback")
def feedback(fb: Feedback):
store.append(fb) # stocké pour le ré-entraînement
return {"status": "recorded"}
Six lignes aujourd'hui — la boucle d'apprentissage de demain.
Guichet 4 · /health
Le pouls du service
GET /health répond « je suis vivant, le modèle est chargé ». Deux lignes de code — une importance démesurée.
Qui appelle ce guichet ? Pas des humains : des machines. Le load balancer, la supervision — et bientôt Kubernetes, qui l'interrogera toutes les dix secondes pour décider s'il faut redémarrer votre service. Rendez-vous au bloc 7.
GET /health{"status": "ok", "model_loaded": true}
L'API complète · vue d'ensemble
Quatre guichets, quatre clients
POST /predict
L'app de paiement
Une transaction, un verdict, quelques millisecondes. Le cœur du service — et du métier.
POST /batch
Les analystes
Des milliers de transactions d'un coup, vectorisées. Le mode batch, offert par la même API.
POST /feedback
Le verdict humain qui revient
La vérité terrain, capturée à la source. Future matière première du ré-entraînement.
GET /health
Les machines qui surveillent
Le pouls, lu par le load balancer aujourd'hui — par Kubernetes au bloc 7.
Acte 4
Prouver que ça marche
pytest · TestClient
Tester l'API sans la lancer
TestClient fait tourner l'API en mémoire : pas de serveur à démarrer, pas de port à ouvrir. Chaque test envoie une vraie requête HTTP et vérifie la réponse — le tout en millisecondes.
Ces tests sont un investissement : au bloc 6, la CI les rejouera à chaque commit, automatiquement. Écrivez-les comme si un robot allait les lire — parce que c'est le cas.
Une transaction valide → 200, un score entre 0 et 1, un verdict cohérent avec le seuil.
Test 2
Le videur
Montant négatif, champ manquant → 422, avec le bon champ désigné. On teste aussi les refus.
Test 3
Le batch
100 transactions envoyées → 100 scores rendus, dans l'ordre. Le contrat vaut aussi pour les listes.
Test 4
Le pouls
/health → 200 et model_loaded: true. Le test le plus court — et le premier que la prod regardera.
Swagger · /docs
La doc qui ne ment jamais
Ouvrez /docs : chaque guichet, chaque champ, chaque exemple — générés depuis le code. La doc manuelle ment au bout de deux semaines ; celle-ci est reconstruite à chaque démarrage, depuis la seule source de vérité : le code.
Et elle est interactive : le bouton « Try it out » envoie de vraies requêtes. La démo de tout à l'heure ? Offerte avec le framework.
# http://localhost:8000/docs
Fraud Detector v0.3.0
POST /predict Score une transaction
POST /batch Score une liste de transactions
POST /feedback Enregistre un verdict humain
GET /health Le pouls du service
Écrite par personne, à jour pour toujours.
Avant de coder · l'essentiel
Cinq réflexes à emporter
Réflexe 1
Le mode de serving vient du métier
Batch quand la décision peut attendre, temps réel quand elle est en cours. Jamais l'inverse.
Réflexe 2
Le modèle se charge au démarrage
Jamais dans la fonction de prédiction. 2 secondes une fois — pas 2 secondes à chaque requête.
Réflexe 3
Valider à la porte
Pydantic déclare le contrat, le 422 fait le reste. Rien de sale ne touche le modèle.
Réflexe 4
Mêmes features au train et au serving
Une seule source de vérité pour le calcul des features — sinon le skew s'installe en silence.
Et le cinquième : tester le contrat, pas les entrailles — c'est le contrat que les autres consomment.
Acte 5 · lab
À vous : 2h30 au clavier
Mission 1 / 3 · 1h20 · pratique
Ouvrir le guichet /predict
Dans src/api/main.py : créer l'app FastAPI, charger votre modèle du bloc 2 au démarrage (lifespan), déclarer POST /predict qui renvoie fraud_score et verdict — avec votre seuil justifié en euros. Lancer avec uvicorn, appeler avec curl.
Livrable : uvicorn src.serve.app:app tourne, et une transaction envoyée en curl reçoit un verdict en moins de 100 ms.
FastAPIuvicornlifespanjoblib.loadcurl
Piège classique : si votre premier appel prend 2 secondes, vous savez exactement où chercher.
Mission 2 / 3 · 0h50 · pratique
Blinder la porte, ouvrir les autres guichets
Écrire le modèle Pydantic Transaction avec ses bornes (montant > 0, heure 0–23), puis ajouter /batch (une liste, scorée vectorisée), /feedback (le verdict humain, stocké) et /health. Et vérifier le videur : envoyez exprès une transaction cassée.
Livrable : les quatre guichets répondent, et une transaction invalide est refusée en 422 avec le champ fautif désigné.
PydanticBaseModelField(gt=0)list[Transaction]422
Binômes : un qui code, un qui attaque l'API avec des requêtes tordues — puis on échange.
Mission 3 / 3 · 0h20 · pratique
Prouver et documenter
Écrire les quatre tests de la checklist avec TestClient — nominal, 422, batch, health — puis ouvrir /docs et essayer chaque guichet depuis Swagger. Commitez le tout : ces tests seront la matière première de la CI au bloc 6.
Livrable : pytest vert (4 tests minimum) et l'API entièrement décrite dans /docs.
pytestTestClient/docsTry it out
20 minutes suffisent : l'API est petite, les tests sont courts — c'est exactement pour ça qu'on les écrit maintenant.
Le fil rouge · fin du bloc 3
Le modèle a une adresse
Regardez la carte : la porte d'entrée est apparue. Les transactions passent par l'API, le modèle répond en temps réel, et le verdict humain revient par /feedback. Le cerveau n'est plus muet.
Mais cette API tourne sur UNE machine — la vôtre. « Ça marche chez moi » n'est pas une stratégie de production.
API en ligne ✓validée ✓testée ✓portable… pas encore
La suite
Faire tourner partout
Bloc 4 — Conteneurisation avec Docker : votre API, ses dépendances et son modèle dans une boîte que n'importe quelle machine sait exécuter.