Module 1 — FastAPI : routes, types et documentation automatique
Le cours 20 s'est terminé sur un artefact déposé dans le registre MLflow :
un modèle de résiliation d'abonnés télécom, alias @champion, capable de
répondre à un tableau de bord Streamlit qui affiche les cent abonnés les
plus à risque. Entre ce modèle et le tableau de bord, il manque une
interface : un serveur HTTP qui accepte un identifiant d'abonné, applique le
modèle et renvoie un score. C'est ce que fait FastAPI, et c'est ce module
qui pose la première pierre du service que les neuf suivants
compléteront.
Pourquoi FastAPI plutôt qu'autre chose
Trois cadres se partagent le monde Python des interfaces web : Flask,
Django et FastAPI. Les deux premiers sont antérieurs à l'idée
d'annotations de types systématiques dans le langage ; FastAPI, sorti en
2018, en fait le socle. La différence pratique est immense.
Avec Flask, on décrit une route, on lit request.json, on convertit
soi-même les champs et on écrit à la main un morceau de code qui refuse les
requêtes mal formées. Avec FastAPI, on décrit un modèle Pydantic (module
2), on l'annote sur la signature de la fonction, et le cadre s'occupe de la
validation, de la conversion et du message d'erreur en cas d'entrée
invalide. Il en tire aussi, gratuitement, une documentation interactive à
/docs que les équipes appelantes ouvrent dans un navigateur pour essayer
la route sans écrire une ligne de code.
FastAPI repose sur ASGI, une norme asynchrone récente qui remplace WSGI,
et se lance avec Uvicorn, un serveur écrit en C sur uvloop et
httptools. Cette combinaison donne, pour un simple aller-retour JSON, des
latences deux à dix fois inférieures à celles de Flask derrière gunicorn
classique. Sur un modèle de scoring, ce gain compte moins que la
robustesse — on passe le plus clair du temps dans predict — mais il ne
disparaît pas.
Une application minimale
Le service commence, comme tous les projets FastAPI, par un fichier
app.py qui n'a besoin de rien d'autre que du cadre et du serveur.
pip install fastapi uvicorn
Le service minimum a dix lignes et expose déjà deux routes.
from fastapi import FastAPI
app = FastAPI(
title="API de scoring — résiliation télécom",
version="0.1.0",
description="Prédit la probabilité qu'un abonné résilie dans les 30 jours.",
)
@app.get("/")
def racine() -> dict:
return {"service": "scoring-resiliation", "version": "0.1.0"}
@app.get("/health")
def sante() -> dict:
return {"status": "ok"}
Deux commandes suffisent à le voir tourner et à voir sa documentation.
uvicorn app:app --host 0.0.0.0 --port 8000 --reload
# puis, dans un navigateur :
# http://localhost:8000/health -> {"status":"ok"}
# http://localhost:8000/docs -> Swagger UI
# http://localhost:8000/redoc -> ReDoc
Le --reload recharge le service à chaque écriture de fichier ; on le
supprime en production, où il consomme du processeur pour rien et complique
le suivi de la mémoire.
Verbes HTTP : GET pour lire, POST pour prédire
Les routes du service final relèvent de deux verbes.
GET est réservé à la lecture d'un objet identifié par son chemin. On l'utilise
pour la santé (/health), la version (/), et — plus tard — la liste des
variables attendues (/schema). Un GET ne modifie rien côté serveur et ne
porte pas de corps de requête.
POST porte un corps et déclenche un calcul. On l'utilise pour la prédiction
(/predict en unitaire, /predict/batch par lots au module 4). Un débutant
est tenté de faire passer les variables de l'abonné en paramètres de la
query string (/predict?age=35&anciennete=12) pour tester dans le
navigateur ; c'est un piège qui casse au premier champ qui contient un
espace, une virgule, un caractère non ASCII ou une liste. Le corps JSON est
la seule voie propre.
Annotations de types : le contrat lisible par la machine
L'apport spécifique de FastAPI est d'utiliser les annotations Python standard pour construire, à l'exécution, la description OpenAPI du service.
from fastapi import FastAPI, Path, Query
app = FastAPI()
@app.get("/abonnes/{abonne_id}")
def lire_abonne(
abonne_id: int = Path(..., ge=1, description="Identifiant interne"),
inclure_historique: bool = Query(False),
) -> dict:
return {"abonne_id": abonne_id, "historique": inclure_historique}
À la lecture, ce code raconte trois choses : l'abonne_id est un entier
positif obtenu dans le chemin ; inclure_historique est un booléen
optionnel obtenu dans la query string ; la réponse est un dictionnaire.
FastAPI en tire trois comportements : il convertit la chaîne du chemin en
int et refuse abonnes/abc avec un code 422, il expose le booléen dans
Swagger comme une case à cocher, et il documente le schéma sans qu'aucun
commentaire manuel n'ait été écrit.
La documentation qui ne peut pas mentir
Swagger UI à /docs et ReDoc à /redoc lisent la même description
OpenAPI, générée à partir des annotations. Elle ne peut donc pas dériver du
code : si la signature de la route change, la documentation change à la
seconde d'après. C'est un renversement radical par rapport aux services où
la documentation vit dans un fichier README.md à part, mis à jour au
mieux, oublié au pire.
L'appelant qui ouvre /docs voit les routes disponibles, les schémas
attendus, un bouton « Try it out » qui envoie une requête réelle, et la
réponse renvoyée par le service. Sur le fil rouge, c'est la première chose
qu'on montrera à l'équipe qui tient le tableau de bord Streamlit : ils
verront l'endpoint, comprendront le schéma sans lire une note, et essaieront
avec deux clics.
Fixer version et titre dès le début
Le constructeur FastAPI(...) accepte title, version et description qui
apparaissent en tête de la documentation. On les remplit tout de suite,
avec la version du service (pas celle du modèle, qui viendra au module 3)
suivant le schéma SemVer : 0.1.0 pour ce cours, 1.0.0 le jour où
l'interface est figée. Une version dans l'URL (/v1/predict) permet, plus
tard, de faire coexister deux interfaces pendant une migration ; on la met
en place dès qu'une route sort de développement.
Les pièges du premier jour
Confondre serveur de développement et serveur de production. --reload
est utile en local ; en production, il crée des rechargements imprévus et
double la mémoire résidente. On utilisera au module 9 uvicorn sans
--reload, avec plusieurs travailleurs.
Renvoyer un dict sans en décrire le schéma. Le code minimal ci-dessus
en abuse pour rester court ; le module 2 remplace tous les -> dict par
des modèles Pydantic typés, ce qui améliore la documentation, permet la
validation de la sortie et attrape les régressions de contrat.
Écrire les paramètres dans l'URL alors qu'ils devraient être dans le
corps. Un identifiant d'abonné a sa place dans le chemin ; les
30 variables d'un dossier client vont dans le corps d'un POST. Le
premier respecte REST ; le second est validé par Pydantic.
Il est utile de garder deux terminaux ouverts en développement : l'un pour
uvicorn app:app --reload, l'autre pour curl ou httpie. La boucle
« modifier, sauvegarder, tester » tombe alors à quelques secondes, ce qui
change concrètement la qualité du code écrit.
En résumé
- FastAPI transforme les annotations Python standard en contrat OpenAPI vivant, sans documentation à maintenir à part.
- Une application minimale tient en dix lignes, se lance avec Uvicorn et
expose immédiatement
/docset/redoc. GETsans corps pour lire,POSTavec corps JSON pour déclencher une prédiction ou tout calcul.- Fixer
titleetversiondès le premier commit ;--reloaden local seulement, jamais en production.
Le module 2 remplace les dict par des modèles Pydantic, ce qui rend la
validation stricte, les erreurs claires et la documentation exemplaire.