Leçon 09-2 — requests et HTTP GET
1. Objectif
À la fin de cette leçon tu sauras : faire une requête HTTP GET avec la bibliothèque requests (toujours avec un timeout), passer des paramètres d'URL proprement avec params=, vérifier le code de statut (200, 404, 429, 500…) avant d'exploiter la réponse, parser le JSON avec response.json(), et écrire un appel réseau qui gère ses échecs (ConnectionError, Timeout, HTTPError) au lieu de planter.
2. Pourquoi c'est important
C'est la leçon où ton Python sort de sa bulle : jusqu'ici, toutes tes données venaient de ton clavier ou de tes fichiers. Avec HTTP, ton script peut interroger la météo, des taux de change, n'importe quelle API du monde — le même mécanisme que ton navigateur utilise à chaque page. Mais le réseau introduit une nouveauté radicale : l'échec devient normal. Serveur lent, wifi coupé, URL fausse, serveur surchargé — un programme réseau sérieux se définit autant par ce qu'il fait quand ça échoue que quand ça marche. Les réflexes appris ici (timeout, statut, try/except) sont exactement ceux de la programmation professionnelle.
3. Explication simple
HTTP, c'est un dialogue question → réponse entre ton script (le client) et un serveur.
Une requête GET dit : « donne-moi ce qui est à cette adresse ». Le serveur répond deux choses : un code de statut (un nombre qui résume comment ça s'est passé) et un corps (les données, souvent du JSON).
import requests
response = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={"latitude": 43.7, "longitude": 7.27, "current": "temperature_2m"},
timeout=10, # OBLIGATOIRE : au-delà de 10 s sans réponse, on abandonne
)
print(response.status_code) # 200 = tout va bien
print(response.json()) # le corps, converti en dicts/listes Python
Les codes de statut à connaître par famille :
| Code | Famille | Sens |
|---|---|---|
| 200 | 2xx | Succès — la réponse contient ce que tu as demandé |
| 404 | 4xx | Ton erreur : la ressource n'existe pas (URL fausse ?) |
| 429 | 4xx | Ton erreur : trop de requêtes, ralentis |
| 500 | 5xx | Leur erreur : le serveur a planté, réessaie plus tard |
Règle mnémotechnique : 4xx = regarde ton code, 5xx = regarde ta montre (attends et réessaie).
4. Explication approfondie
a) Anatomie d'une URL avec paramètres. L'adresse https://api.open-meteo.com/v1/forecast?latitude=43.7&longitude=7.27 se découpe en : le serveur (api.open-meteo.com), le chemin (/v1/forecast), et après le ?, les paramètres clé=valeur séparés par des &. Tu POURRAIS construire cette chaîne à la main avec une f-string — ne le fais pas : certains caractères (espaces, accents, & dans une valeur…) doivent être encodés selon des règles précises. params= (un dict) s'en charge :
params = {"name": "São Paulo", "count": 1}
# requests encodera correctement name=S%C3%A3o%20Paulo&count=1
b) Pourquoi timeout est non négociable. Sans lui, requests.get(url) peut attendre indéfiniment un serveur qui ne répondra jamais — ton script est gelé, sans erreur, sans rien. Un script d'automatisation qui tourne la nuit et gèle à 2 h du matin, c'est une nuit perdue. Dans cette méthode, tout appel réseau a un timeout ; un exemple sans timeout est un exemple faux. À l'expiration, requests lève requests.exceptions.Timeout — une vraie exception que ton try/except peut attraper, contrairement au gel.
c) La hiérarchie des exceptions réseau. Toutes héritent de requests.exceptions.RequestException (⏩ l'héritage : niveau 06) :
RequestException ← attrape tout ce qui suit
├── ConnectionError ← pas de réseau, DNS introuvable, serveur injoignable
├── Timeout ← le délai timeout= est écoulé
└── HTTPError ← levée par raise_for_status() sur un 4xx/5xx
Tu peux donc attraper finement (un except par cas, messages précis) ou globalement (except requests.exceptions.RequestException) — les deux s'utilisent, selon que tu veux réagir différemment ou pas.
d) status_code vs raise_for_status(). Point crucial : un 404 ou un 500 ne lève aucune exception tout seul. requests.get a fait son travail (demander, recevoir une réponse) ; que la réponse soit une erreur HTTP, c'est à toi de le vérifier. Deux styles :
# Style 1 : tester soi-même
if response.status_code == 200:
data = response.json()
# Style 2 : convertir les erreurs HTTP en exception (recommandé)
response.raise_for_status() # ne fait RIEN si 2xx ; lève HTTPError si 4xx/5xx
data = response.json()
Le style 2 s'intègre naturellement au try/except qui protège déjà l'appel — un seul chemin d'erreur pour tout.
e) response.json() peut échouer aussi. Il convertit le corps de la réponse en objets Python — SI ce corps est du JSON valide. Une page d'erreur HTML, un corps vide → requests.exceptions.JSONDecodeError. Raison de plus de vérifier le statut AVANT de parser.
f) Note honnête : SSLError derrière un antivirus ou un proxy. Sur certaines machines (antivirus avec « inspection HTTPS », réseau d'entreprise), tes premiers appels peuvent échouer avec requests.exceptions.SSLError: certificate verify failed. Ce que ça veut dire : HTTPS chiffre le dialogue et vérifie l'identité du serveur via un certificat ; un antivirus/proxy qui inspecte le trafic s'intercale et présente SON propre certificat, que Python ne connaît pas — la vérification échoue, et c'est elle qui te protège. Tu croiseras sur internet le « remède » verify=False : il désactive purement et simplement cette vérification d'identité. Ne code jamais ça — c'est ouvrir la porte aux interceptions malveillantes, pas corriger le problème. Les vraies pistes : essayer un autre réseau (partage de connexion mobile) pour confirmer le diagnostic, ou faire confiance au magasin de certificats du système (pip install truststore puis deux lignes d'activation, voir sa doc). Si tu vois cette erreur : ton code est probablement correct, c'est ton réseau qui est particulier.
Difficulté honnête : la nouveauté ici n'est pas la syntaxe (5 lignes), c'est le changement de posture — coder en supposant que ça va échouer. Compte deux sessions : une pour les appels qui marchent, une pour provoquer et gérer tous les échecs.
5. Exemples commentés
Tous les exemples supposent le venv de la leçon 09-1 activé, avec requests installé. L'API utilisée est Open-Meteo : météo mondiale, gratuite, sans clé d'inscription.
Exemple 1 — premier GET, inspection de la réponse
import requests
response = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={"latitude": 43.7, "longitude": 7.27, "current": "temperature_2m"},
timeout=10,
)
print(response.status_code) # 200
print(response.url) # l'URL finale, paramètres encodés — utile pour déboguer
print(response.json())
Sortie (abrégée, tes valeurs différeront) :
200
https://api.open-meteo.com/v1/forecast?latitude=43.7&longitude=7.27¤t=temperature_2m
{'latitude': 43.7, 'longitude': 7.27, ..., 'current': {'time': '2026-07-05T21:45', 'interval': 900, 'temperature_2m': 26.7}}
response.json() renvoie un dict Python ordinaire : à partir d'ici, plus de réseau, que du niveau 03.
Exemple 2 — extraire une valeur du dict de réponse
import requests
response = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": 43.7,
"longitude": 7.27,
"current": "temperature_2m,wind_speed_10m", # plusieurs mesures
},
timeout=10,
)
response.raise_for_status() # 4xx/5xx → exception, sinon ne fait rien
data = response.json()
current = data["current"] # dict imbriqué (⏩ navigation JSON : leçon 09-3)
print(f"Température : {current['temperature_2m']} °C")
print(f"Vent : {current['wind_speed_10m']} km/h")
Sortie :
Température : 26.7 °C
Vent : 1.0 km/h
Exemple 3 — provoquer un 404 et un 400 pour les voir en vrai
import requests
# URL au chemin inexistant → 404
r1 = requests.get("https://api.open-meteo.com/v1/nowhere", timeout=10)
print(r1.status_code) # 404 — et AUCUNE exception levée !
# paramètre invalide → 400 (Bad Request)
r2 = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={"latitude": "abc"}, # une latitude en lettres…
timeout=10,
)
print(r2.status_code) # 400
print(r2.json()) # beaucoup d'APIs expliquent l'erreur dans le corps
Sortie :
404
400
{'reason': "Data corrupted at path ''. Cannot initialize Float from invalid String value abc.", 'error': True}
Leçon : le corps d'une réponse d'erreur contient souvent le POURQUOI — affiche-le quand tu débogues.
Exemple 4 — l'appel robuste complet (le modèle à retenir)
import requests
def fetch_current_temperature(latitude, longitude):
"""Return the current temperature, or None if anything fails."""
try:
response = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={"latitude": latitude, "longitude": longitude,
"current": "temperature_2m"},
timeout=10,
)
response.raise_for_status() # HTTPError si 4xx/5xx
return response.json()["current"]["temperature_2m"]
except requests.exceptions.Timeout:
print("Le serveur n'a pas répondu à temps. Réessaie plus tard.")
except requests.exceptions.ConnectionError:
print("Connexion impossible. Vérifie ton accès internet.")
except requests.exceptions.HTTPError as error:
print(f"Le serveur a répondu une erreur : {error}")
return None # échec → valeur sentinelle
temp = fetch_current_temperature(43.7, 7.27)
if temp is not None:
print(f"Il fait {temp} °C à Nice.")
else:
print("Pas de météo cette fois-ci.")
Sortie (cas nominal) :
Il fait 26.7 °C à Nice.
Structure à mémoriser : l'appel ET raise_for_status() ET .json() dans le try ; un except par famille d'échec ; la fonction renvoie None en cas de problème et l'appelant décide quoi faire (⏩ séparation des responsabilités : niveau 04).
Exemple 5 — provoquer un Timeout volontairement
import requests
try:
# 1 milliseconde : aucune API ne répond aussi vite → Timeout garanti
requests.get("https://api.open-meteo.com/v1/forecast",
params={"latitude": 43.7, "longitude": 7.27},
timeout=0.001)
except requests.exceptions.Timeout:
print("Timeout attrapé — le programme continue proprement.")
Sortie :
Timeout attrapé — le programme continue proprement.
Provoquer ses erreurs en environnement contrôlé, c'est le meilleur moyen de reconnaître leurs tracebacks le jour où elles arrivent pour de vrai.
6. Erreurs fréquentes
1) Pas de timeout
response = requests.get(url) # gèle potentiellement POUR TOUJOURS
Aucun message d'erreur — c'est pire : un gel silencieux, souvent découvert en production. Correction : timeout=10 (ou une valeur adaptée) sur CHAQUE appel. Si tu ne devais retenir qu'une règle de cette leçon, c'est celle-ci.
2) Appeler .json() sans vérifier le statut
response = requests.get(bad_url, timeout=10) # renvoie un 404
data = response.json() # et si le corps est du HTML…
# requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
Pire cas : le corps d'erreur EST du JSON valide ({'error': True, ...}) — pas d'exception, et ton code traite un message d'erreur comme des données météo. Bug silencieux. Correction : raise_for_status() avant .json(), systématiquement.
3) Construire l'URL à la main avec une f-string
city = "São Paulo"
url = f"https://geocoding-api.open-meteo.com/v1/search?name={city}&count=1"
# l'espace et le ã ne sont pas encodés → résultats faux ou 400 selon l'API
Correction : requests.get(base_url, params={"name": city, "count": 1}, timeout=10) — l'encodage est le travail de requests, pas le tien.
4) except trop large qui avale tout
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
print(data["curent"]) # faute de frappe dans la clé !
except Exception:
print("Erreur réseau") # FAUX : c'était un KeyError, pas le réseau
Le except Exception masque le vrai bug (la faute de frappe) derrière un message mensonger. Correction : n'attraper QUE les exceptions de requests (requests.exceptions.RequestException au plus large) ; un KeyError doit planter bruyamment pour que tu le voies (⏩ règle vue au niveau 05).
5) Confondre l'exception ConnectionError de requests et la native
try:
requests.get("https://nexistepas-vraiment-pas.example", timeout=10)
except ConnectionError: # ← la ConnectionError NATIVE de Python !
print("attrapé ?") # ...souvent oui, mais par accident
Il existe une ConnectionError native ET une requests.exceptions.ConnectionError (qui, par chance, hérite de la première — ça « marche » donc parfois par accident). Écris toujours le chemin complet requests.exceptions.ConnectionError : explicite, sans ambiguïté, et cohérent avec Timeout et HTTPError qui, EUX, ne seraient pas attrapés par des noms natifs.
6) Marteler une API en boucle → 429
for city in cities: # 500 villes…
requests.get(api_url, params={"name": city}, timeout=10) # …sans pause
Réponse du serveur au bout d'un moment : 429 Too Many Requests. Les APIs gratuites ont des limites (Open-Meteo : généreuses mais réelles). Correction : espacer les appels (time.sleep(1) entre chaque), et si un 429 arrive, attendre avant de réessayer — pas réessayer immédiatement en boucle, ce qui aggrave le cas.
7. Lire les messages d'erreur
ConnectionError — le réseau est coupé ou l'hôte n'existe pas
Traceback (most recent call last):
...(des dizaines de lignes internes à urllib3 et requests)...
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='api.open-meteo.com', port=443):
Max retries exceeded with url: /v1/forecast?latitude=43.7
(Caused by NameResolutionError(... Failed to resolve 'api.open-meteo.com' ...))
Lecture (de bas en haut) :
- Dernière ligne : le type (
ConnectionError) et, dans la parenthèseCaused by, la cause racine.NameResolutionError/Failed to resolve= le DNS ne trouve pas l'hôte : faute de frappe dans le nom de domaine, ou pas d'internet du tout. - La longueur du traceback est normale :
requestss'appuie sururllib3, chaque couche ajoute ses lignes. Ne lis pas tout : saute à la dernière ligne, puis auCaused by. - Vérifie dans l'ordre : as-tu internet (ouvre un site dans le navigateur) ? l'hôte est-il bien orthographié ? le
https://est-il présent ? (sans lui :MissingSchema: Invalid URL ... No scheme supplied. Perhaps you meant https://...— celle-là dit littéralement quoi corriger).
HTTPError — le serveur a répondu, mais mal
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url:
https://api.open-meteo.com/v1/forecast?latitude=abc
Tout y est : le code (400), la famille (Client Error = 4xx = ton côté), et l'URL complète avec les paramètres encodés — relis-la lentement, la faute y est souvent visible (ici : latitude=abc).
SSLError — la vérification de certificat a échoué
requests.exceptions.SSLError: HTTPSConnectionPool(host='api.open-meteo.com', port=443):
Max retries exceeded with url: /v1/forecast
(Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: unable to get local issuer certificate ...')))
Relis la section 4-f : sur une machine avec antivirus inspecteur de HTTPS ou derrière un proxy d'entreprise, cette erreur ne signifie PAS que ton code est faux. Diagnostic : le même script sur un autre réseau (partage mobile) fonctionne-t-il ? Remède propre : truststore. Remède interdit : verify=False.
8. Exercices — faciles
Règles : fichiers dans
exercices/mes-reponses/, nommésex-09-2-a.py, etc. Venv activé,requestsinstallé (leçon 09-1).timeout=partout — un exercice rendu sans timeout est un exercice faux.
a) Fais un GET sur https://api.open-meteo.com/v1/forecast avec les paramètres latitude=48.85, longitude=2.35 (Paris) et current=temperature_2m, passés via params=. Affiche : le code de statut, l'URL finale (response.url), et la température extraite du JSON.
b) Reprends (a) mais demande une URL au chemin faux (/v1/forecastttt). Affiche le code de statut et le corps (response.text). Le programme ne doit PAS planter — tu n'appelles pas .json() ici. Note dans ton journal : pourquoi n'y a-t-il pas eu d'exception ?
c) Provoque un Timeout avec timeout=0.001, attrape-le avec le chemin d'exception complet, et affiche un message propre. Puis remets timeout=10 et vérifie que l'appel passe.
9. Exercices — moyens
a) Écris une fonction get_wind_speed(latitude, longitude) qui renvoie la vitesse du vent actuelle (wind_speed_10m demandé dans current), ou None en cas d'échec réseau ou HTTP. Trois except distincts (Timeout, ConnectionError, HTTPError), chacun avec un message différent. Teste les trois branches : timeout ridicule, hôte inexistant, paramètre invalide (latitude="abc").
b) L'API de géocodage d'Open-Meteo transforme un nom de ville en coordonnées : https://geocoding-api.open-meteo.com/v1/search avec les paramètres name (la ville) et count=1. Écris find_city(name) qui affiche le nom, le pays, la latitude et la longitude du premier résultat. Piège intégré : pour une ville inexistante ("Zzzzz"), la réponse est un 200 dont le JSON n'a pas de clé "results" — ton code doit l'annoncer proprement au lieu de planter (.get() est ton ami, ⏩ approfondi en 09-3).
c) Chaîne les deux : weather_for(city_name) appelle find_city puis la météo aux coordonnées trouvées, et affiche À <ville> (<pays>) : <temp> °C, vent <vent> km/h. Deux appels réseau = deux occasions d'échouer : l'échec du premier doit empêcher le second, avec un message clair à chaque étage.
10. Exercices — difficiles
a) Écris fetch_with_retry(url, params, attempts=3) : elle tente l'appel jusqu'à attempts fois. Règles : sur Timeout ou ConnectionError, elle attend 2 ** attempt secondes (1 s, 2 s, 4 s — un « backoff exponentiel ») puis réessaie ; sur une erreur HTTP 4xx, elle abandonne IMMÉDIATEMENT ( réessayer une URL fausse ne la corrigera pas) ; sur un 5xx, elle réessaie. Elle renvoie le dict JSON ou None. Teste avec un timeout ridicule pour voir les tentatives défiler.
- Indice 1 :
time.sleep(seconds)pour attendre ; une bouclefor attempt in range(attempts). - Indice 2 : pour distinguer 4xx de 5xx dans le
except HTTPError as e:e.response.status_codete donne le code ;400 <= code < 500= abandon. - Indice 3 : structure —
trycomplet dans la boucle ;returndès le succès ;except HTTPError: si 4xx →return None, sinon on laisse la boucle continuer ; après la boucle,return None.
b) Baromètre multi-villes : un script qui prend plusieurs villes (["Nice", "Paris", "Zzzz", "Tokyo"]), interroge la météo de chacune via ta fonction de l'exercice 9-c, espace les appels de 1 seconde (politesse envers l'API), et affiche un tableau final : une ligne par ville, ? pour celles en échec. LE point noté : une ville en échec ne doit jamais empêcher les suivantes.
11. Mini-projet lié — « météo du terminal »
C'est le grand projet du niveau, sur deux leçons : commence-le maintenant, tu le termineras en 09-3. Fiche complète : projects/mini-projects/11-meteo-api.md.
Étape « leçon 09-2 » (30–45 min) : dans ton dossier meteo-projet/ de la leçon 09-1, écris weather.py qui prend une ville en argument (sys.argv, ⏩ niveau 05), la géocode, affiche la température et le vent actuels, et survit proprement aux trois pannes : pas de réseau, ville inconnue, serveur en erreur. Les prévisions multi-jours et le JSON imbriqué attendront la leçon 09-3.
12. Correction / méthode de correction
Les solutions détaillées sont dans solutions/serie-09-2-solutions.md.
Méthode : (1) finis TOUS les exercices avant d'ouvrir les solutions ; (2) compare surtout la structure des try/except : quoi est dans le try, quelles exceptions sont attrapées, dans quel ordre ; (3) vérifie mécaniquement : chaque requests.get de ton code a-t-il son timeout ? son contrôle de statut ? (4) note dans ton journal chaque erreur réseau rencontrée en vrai et sa dernière ligne de traceback — c'est ton dictionnaire personnel des pannes.
13. À retenir
requests.get(url, params=..., timeout=...): le trio de base — ettimeoutest obligatoire, toujours.params=(un dict) construit et encode l'URL ; jamais de f-string pour ça.- Codes de statut : 200 OK, 404 introuvable, 429 ralentis, 500 panne serveur. 4xx = ton côté, 5xx = le leur.
- Un 404/500 ne lève PAS d'exception tout seul :
raise_for_status()le convertit enHTTPError. - L'ordre sacré : GET →
raise_for_status()→.json(), le tout dans untry. - Exceptions à connaître :
requests.exceptions.Timeout,.ConnectionError,.HTTPError— toutes filles deRequestException. - Traceback réseau : dernière ligne d'abord, puis le
Caused by. SSLErrorderrière un antivirus/proxy = problème d'environnement, pas de code. Jamaisverify=False.
14. Questions de révision
- Que risque exactement un appel
requests.get(url)sanstimeout, et pourquoi est-ce pire qu'une exception ? - Pourquoi passer les paramètres via
params=plutôt que dans une f-string ? - Que fait
raise_for_status()sur un 200 ? Sur un 404 ? - Quelle est la différence de nature entre un code 4xx et un code 5xx, et qu'est-ce que ça change pour une logique de « réessai » ?
- Cite les trois exceptions réseau vues et la situation qui déclenche chacune.
- Pourquoi un
except Exception:autour d'un appel réseau est-il une mauvaise idée ? - Ton script recevait des 200 hier et reçoit des 429 aujourd'hui : que s'est-il passé et que fais-tu ?
- Que signifie une
SSLErrorsur une machine avec antivirus « inspecteur HTTPS », et pourquoiverify=Falseest-il la mauvaise réponse ?
15. Checklist de compréhension
- Je peux écrire un GET avec
paramsettimeoutsans regarder la leçon. - Je connais 200, 404, 429, 500 et je sais qui est fautif à chaque fois.
- Je place
raise_for_status()au bon endroit sans réfléchir. - Je peux provoquer volontairement un
Timeout, un 400 et uneConnectionError, et lire leurs tracebacks. - Ma fonction d'appel réseau renvoie une valeur sentinelle en cas d'échec et l'appelant décide.
- Je peux expliquer à quelqu'un pourquoi on n'écrit jamais
verify=False. - J'ai commencé le mini-projet météo et il survit à une coupure réseau.
Si une case reste vide : refais les exercices de la section correspondante demain, AVANT d'ouvrir la leçon suivante.
16. Commit conseillé
git add lessons/level-09-automatisation-apis-donnees/exercices/mes-reponses/
git commit -m "exercises: complete lesson 09-2 requests and HTTP GET"
Puis, si c'est ta fin de session : entrée de journal + git push.