Aller au contenu principal

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.

Deux processus, un même dépôt

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 /docs et /redoc.
  • GET sans corps pour lire, POST avec corps JSON pour déclencher une prédiction ou tout calcul.
  • Fixer title et version dès le premier commit ; --reload en 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.