
Au-delà des requêtes : pourquoi httpx est le client HTTP moderne dont vous avez besoin (parfois)
Chaque fois que vous effectuez des appels HTTP en Python, il y a de fortes chances que vous ayez utilisé la bibliothèque Requests. Depuis de nombreuses années, Requests est le de facto standard, connu pour sa relative simplicité et qui a été la pierre angulaire d’innombrables applications Python. Des scripts simples aux services Web plus complexes, sa nature synchrone fonctionne bien dans de nombreux types d’applications différents.
Cependant, l’écosystème des bibliothèques Python évolue constamment, notamment avec l’essor de la programmation asynchrone utilisant asyncio. Ce changement a ouvert la porte à de nouvelles bibliothèques conçues pour exploiter les E/S non bloquantes pour des performances améliorées, en particulier dans les applications liées aux E/S.
C’est là qu’intervient la bibliothèque HTTPX, un nouveau venu qui se présente comme un « client HTTP de nouvelle génération pour Python », offrant à la fois des API synchrones et asynchrones, ainsi que la prise en charge de fonctionnalités Web modernes telles que HTTP/2.
Qu’est-ce que les requêtes ?
Pour ceux qui découvrent Python ou qui ont besoin d’un rafraîchissement, Requests est une bibliothèque HTTP simple et élégante pour Python, créée par Kenneth Reitz il y a près de quinze ans. Son objectif principal est de rendre les requêtes HTTP simples et conviviales. Vous souhaitez envoyer des données ? Faire une requête GET ou POST ? Gérer les en-têtes, les cookies ou l’authentification ? Les requêtes rendent ces tâches intuitives.
Sa nature synchrone signifie que lorsque vous faites une requête, votre programme attend la réponse avant de passer à autre chose. Cela convient à de nombreuses applications, mais pour les tâches nécessitant de nombreux appels HTTP simultanés (telles que le web scraping ou l’interaction avec plusieurs microservices), ce comportement de blocage peut devenir un goulot d’étranglement important.
Qu’est-ce que HTTPX ?
Selon sa documentation officielle, HTTPX est un,
«… Client HTTP complet pour Python 3, qui fournit des API de synchronisation et asynchrones, ainsi que la prise en charge de HTTP/1.1 et HTTP/2.»
Il a été développé par Encode (l’équipe derrière Starlette, Uvicorne et Cadre de repos Django).
Certains des arguments de vente de HTTPX incluent :
- Prise en charge asynchrone : Syntaxe native async/wait pour les opérations non bloquantes.
- Prise en charge HTTP/2 : Contrairement aux requêtes (qui prennent principalement en charge HTTP/1.1 par défaut), HTTPX peut parler HTTP/2, offrant potentiellement des avantages en termes de performances comme le multiplexage.
- API de type requête : Il vise à fournir une API familière à ceux qui sont habitués aux requêtes, facilitant ainsi la transition.
- API de transport : Une fonctionnalité plus avancée permettant un comportement de transport personnalisé, utile pour les tests ou les configurations réseau spécifiques.
Les revendications concernant HTTPX sont intrigantes. Une API compatible avec les requêtes avec la puissance de l’async/wait et des gains de performances potentiels. Mais est-ce l’héritier présomptif, capable de renverser le champion en titre, ou s’agit-il d’un outil de niche pour des cas d’utilisation asynchrones spécifiques ? Il n’y a qu’une seule façon de le savoir. Mettons-les tous les deux à l’épreuve.
Mise en place d’un environnement de développement
Avant de commencer à coder, nous devons configurer un environnement de développement distinct pour chaque projet sur lequel nous travaillons. J’utilise conda, mais n’hésitez pas à utiliser la méthode qui vous convient.
# Create our test environment (Python 3.7+ is recommended for async features)
# And activate it
(base) $ conda create -n httpx_test python=3.11 -y
(base) $ conda activate httpx_test
Maintenant que notre environnement est actif, installons les bibliothèques nécessaires :
(httpx_test) $ pip install requests httpx[http2] asyncio aiohttp uvicorn fastapi jupyter nest_asyncio
J’utilise Jupyter pour mon code, donc si vous suivez, tapez Carnet Jupyter dans votre invite de commande. Vous devriez voir un notebook Jupyter ouvert dans votre navigateur. Si cela ne se produit pas automatiquement, vous verrez probablement un écran d’informations après la commande Jupyter Notebook. En bas, vous trouverez une URL que vous devez copier et coller dans votre navigateur pour lancer Jupyter Notebook.
Votre URL sera différente de la mienne, mais elle devrait ressembler à ceci : –
http://127.0.0.1:8888/tree?token=3b9f7bd07b6966b41b68e2350721b2d0b6f388d248cc69da
Comparaison des performances HTTPX et des requêtes
Pour comparer les performances, nous allons exécuter une série de requêtes HTTP GET en utilisant les deux bibliothèques et les chronométrer. Nous examinerons d’abord les opérations synchrones, puis les capacités asynchrones.
Pour notre cible, nous utiliserons httpbin.orgun service fantastique pour tester les requêtes HTTP. Considérez-le comme un outil de test et de débogage pour les développeurs qui créent ou travaillent avec des logiciels qui effectuent des requêtes HTTP (comme des clients Web, des clients API, des scrapers, etc.). Au lieu d’avoir à configurer votre propre serveur Web pour voir à quoi ressemblent vos requêtes HTTP ou pour tester la façon dont votre client gère les différentes réponses du serveur, vous pouvez envoyer vos requêtes à un serveur de test sur httpbin.org. Il dispose d’une variété de points de terminaison conçus pour renvoyer des types spécifiques de réponses, vous permettant d’inspecter et de vérifier le comportement de votre client.
Configuration du serveur FastAPI local
Créons une simple application FastAPI pour servir de point de terminaison asynchrone. Enregistrez ceci sous test_server.py :
# test_server.py
from fastapi import FastAPI
import asyncio
app = FastAPI()
@app.get("/fast")
async def read_fast():
return {"message": "Hello from FastAPI!"}
@app.get("/slow")
async def read_slow():
await asyncio.sleep(0.1) # Simulate some I/O-bound work
return {"message": "Hello slowly from FastAPI!"}
Démarrez ce serveur dans une fenêtre de terminal distincte en tapant cette commande.
uvicorn test_server:app --reload --host 127.0.0.1 --port 8000
Nous avons mis en place tout ce dont nous avons besoin. Commençons par nos exemples de code.
Exemple 1 — Requête GET synchrone simple
Commençons par un scénario de base : récupérer une simple réponse JSON 20 fois de manière séquentielle.
import requests
import httpx
import time
import nest_asyncio
nest_asyncio.apply()
URL = "https://httpbin.org/get"
NUM_REQUESTS = 20
# --- Requests ---
start_time_requests = time.perf_counter()
for _ in range(NUM_REQUESTS):
response = requests.get(URL)
assert response.status_code == 200
end_time_requests = time.perf_counter()
time_requests = end_time_requests - start_time_requests
print(f"Execution time (Requests, Sync): {time_requests:.4f} seconds")
# --- HTTPX (Sync Client) ---
start_time_httpx_sync = time.perf_counter()
with httpx.Client() as client: # Using a client session is good practice
for _ in range(NUM_REQUESTS):
response = client.get(URL)
assert response.status_code == 200
end_time_httpx_sync = time.perf_counter()
time_httpx_sync = end_time_httpx_sync - start_time_httpx_sync
print(f"Execution time (HTTPX, Sync): {time_httpx_sync:.4f} seconds")
La sortie.
Execution time (Requests, Sync): 22.6370 seconds
Execution time (HTTPX, Sync): 11.4099 seconds
C’est déjà une amélioration décente par rapport à HTTPX par rapport aux requêtes. C’est presque deux fois plus rapide en récupération synchrone dans notre test.
Exemple 2 — Requête GET asynchrone simple (requête unique) en utilisant HTTPX
Testons maintenant les capacités asynchrones de HTTPX en effectuant une seule requête au serveur FastAPI local que nous avons configuré auparavant.
import httpx
import asyncio
import time
LOCAL_URL_FAST = "http://127.0.0.1:8000/fast"
async def fetch_with_httpx_async_single():
async with httpx.AsyncClient() as client:
response = await client.get(LOCAL_URL_FAST)
assert response.status_code == 200
start_time_httpx_async = time.perf_counter()
asyncio.run(fetch_with_httpx_async_single())
end_time_httpx_async = time.perf_counter()
time_httpx_async_val = end_time_httpx_async - start_time_httpx_async
print(f"Execution time (HTTPX, Async Single): {time_httpx_async_val:.4f} seconds")
La sortie.
Execution time (HTTPX, Async Single): 0.0319 seconds
C’est rapide, comme prévu pour une demande locale. Ce test vérifie principalement que la machinerie asynchrone fonctionne. Le véritable test de l’asynchrone vient de la concurrence.
Exemple 3 — Requêtes GET asynchrones simultanées.
C’est là que les capacités asynchrones de HTTPX devraient vraiment briller sur les requêtes. Nous ferons 100 demandes à notre /lent point final simultanément.
import httpx
import asyncio
import time
import requests
LOCAL_URL_SLOW = "http://127.0.0.1:8001/slow" # 0.1s delay
NUM_CONCURRENT_REQUESTS = 100
# --- HTTPX (Async Client, Concurrent) ---
async def fetch_one_httpx(client, url):
response = await client.get(url)
return response.status_code
async def main_httpx_concurrent():
async with httpx.AsyncClient() as client:
tasks = [fetch_one_httpx(client, LOCAL_URL_SLOW) for _ in range(NUM_CONCURRENT_REQUESTS)]
results = await asyncio.gather(*tasks)
for status_code in results:
assert status_code == 200
start_time_httpx_concurrent = time.perf_counter()
asyncio.run(main_httpx_concurrent())
end_time_httpx_concurrent = time.perf_counter()
time_httpx_concurrent_val = end_time_httpx_concurrent - start_time_httpx_concurrent
print(f"Execution time (HTTPX, Async Concurrent to /slow): {time_httpx_concurrent_val:.4f} seconds")
# --- For Comparison: Requests (Sync, Sequential to /slow) ---
# This will be slow, demonstrating the problem async solves
start_time_requests_sequential_slow = time.perf_counter()
for _ in range(NUM_CONCURRENT_REQUESTS):
response = requests.get(LOCAL_URL_SLOW)
assert response.status_code == 200
end_time_requests_sequential_slow = time.perf_counter()
time_requests_sequential_slow_val = end_time_requests_sequential_slow - start_time_requests_sequential_slow
print(f"Execution time (Requests, Sync Sequential to /slow): {time_requests_sequential_slow_val:.4f} seconds")
Sortie typique
Execution time (HTTPX, Async Concurrent to /slow): 0.1881 seconds
Execution time (Requests, Sync Sequential to /slow): 10.1785 seconds
Maintenant ce c’est pas trop mal ! HTTPX exploitant asyncio.gather a traité 100 requêtes (chacune avec un délai simulé de 0,1 seconde) en un peu plus d’une seconde. Étant donné que les tâches sont liées aux E/S, asyncio peut basculer entre elles en attendant la réponse du serveur. La durée totale correspond à peu près à la durée de la requête individuelle la plus longue, plus une légère surcharge liée à la gestion de la simultanéité.
En revanche, le code des requêtes synchrones prenait plus de 10 secondes (100 requêtes * 0,1 s/requête = 10 secondes, plus surcharge). Cela démontre la puissance des opérations asynchrones pour les tâches liées aux E/S. HTTPX n’est pas seulement « plus rapide » dans un sens absolu ; il permet une manière fondamentalement plus efficace de gérer les E/S simultanées.
Et HTTP/2 ?
HTTPX prend en charge HTTP/2 si le serveur le prend également en charge et que la bibliothèque h2 est installée (pip install httpx[h2]). HTTP/2 offre des avantages tels que le multiplexage (envoi de plusieurs requêtes sur une seule connexion) et la compression d’en-tête.
import httpx
import asyncio
import time
# A public server that supports HTTP/2
HTTP2_URL = "https://github.com"
# HTTP2_URL = "https://www.cloudflare.com" # Another option
NUM_HTTP2_REQUESTS = 20
async def fetch_http2_info():
async with httpx.AsyncClient(http2=True) as client: # Enable HTTP/2
for _ in range(NUM_HTTP2_REQUESTS):
response = await client.get(HTTP2_URL)
# print(f"HTTP Version: {response.http_version}, Status: {response.status_code}")
assert response.status_code == 200
assert response.http_version in ["HTTP/2", "HTTP/2.0"] # Check if HTTP/2 was used
start_time = time.perf_counter()
asyncio.run(fetch_http2_info())
end_time = time.perf_counter()
print(f"Execution time (HTTPX, Async with HTTP/2): {end_time - start_time:.4f} seconds for {NUM_HTTP2_REQUESTS} requests.")
La sortie
Execution time (HTTPX, Async with HTTP/2): 0.7927 seconds for 20 requests.
Bien que ce test confirme l’utilisation de HTTP/2, quantifier ses avantages en termes de vitesse par rapport à HTTP/1.1 dans un simple script peut s’avérer un peu délicat. Les avantages de HTTP/2 deviennent souvent plus évidents dans des scénarios complexes comportant de nombreuses petites ressources ou sur des connexions à latence élevée. Pour de nombreuses interactions API courantes, la différence n’est peut-être pas dramatique à moins que le serveur ne soit spécifiquement optimisé pour exploiter fortement les fonctionnalités HTTP/2. Cependant, disposer de cette capacité constitue une fonctionnalité prospective importante.
Au-delà de la vitesse brute
La performance ne fait pas tout. L’expérience des développeurs, les fonctionnalités et la facilité d’utilisation sont cruciales, examinons donc certains d’entre eux dans notre comparaison des deux bibliothèques.
Prise en charge asynchrone/attente
HTTPX. Support natif de première classe. C’est son différenciateur le plus important.
DEMANDES. Purement synchrone. Pour obtenir un comportement asynchrone avec une sensation de type Requests, vous vous tournerez généralement vers des bibliothèques comme aiohttp (qui a une API différente) ou utiliserez Requests dans un exécuteur de pool de threads (ce qui ajoute de la complexité et n’est pas un vrai asyncio).
Prise en charge HTTP/2
HTTPX. Nous l’avons déjà évoqué, mais pour rappel, cette fonctionnalité est intégrée.
DEMANDES. Pas de support natif HTTP/2. Des adaptateurs tiers existent, mais ne sont pas aussi intégrés.
Conception d’API et facilité d’utilisation
HTTPX. Intentionnellement conçu pour être très similaire aux requêtes. Si vous êtes familier avec les requêtes, HTTPX vous semblera familier. Voici une comparaison rapide des codes.
# Requests
r = requests.get('https://example.com', params={'key': 'value'})
# HTTPX (sync)
r = httpx.get('https://example.com', params={'key': 'value'})
# HTTPX (async)
async with httpx.AsyncClient() as client:
DEMANDES. La référence en matière de simplicité dans les appels HTTP synchrones.
Sessions client/regroupement de connexions
Les deux bibliothèques encouragent fortement l’utilisation de sessions client (requests.Session() et httpx.Client() / httpx.AsyncClient()) pour des avantages en termes de performances, tels que le regroupement de connexions et la persistance des cookies. L’utilisation des deux est très similaire.
Empreinte de dépendance
DEMANDES. Relativement léger (charset_normalizer, idna, urllib3, certifi).
HTTPX. Possède quelques dépendances principales supplémentaires (httpcore, sniffio, anyio, certifi, idna) et h11 pour HTTP/1.1. Si vous ajoutez h2 pour HTTP/2, c’en est une autre. Cela est compréhensible compte tenu de son ensemble de fonctionnalités plus large.
Maturité et communauté
DEMANDES. Communauté extrêmement mature et massive, éprouvée sur une décennie.
HTTPX. Plus jeune mais activement développé par une équipe réputée (Encode) et gagne rapidement du terrain. Il est considéré comme stable et prêt pour la production.
Quand choisir HTTPX ? Quand s’en tenir aux demandes ?
Cela dit, comment choisir entre les deux ? Voici ce que je pense.
Choisissez HTTPX si…
- Vous avez besoin d’opérations asynchrones. C’est la raison principale. Si votre application implique de nombreux appels HTTP liés aux E/S, HTTPX avec asyncio offrira des améliorations significatives des performances et une meilleure utilisation des ressources.
- Vous avez besoin du support HTTP/2. Si vous interagissez avec des serveurs qui exploitent HTTP/2 pour les performances, HTTPX le fournit immédiatement.
- Vous démarrez un nouveau projet et souhaitez le pérenniser. La conception moderne et les capacités asynchrones de HTTPX en font un choix judicieux pour les nouvelles applications.
- Vous souhaitez une bibliothèque unique pour les appels HTTP synchronisés et asynchrones. Cela peut simplifier votre gestion des dépendances et votre base de code si vous avez des besoins mixtes.
- Vous avez besoin de fonctionnalités avancées. Comme son API Transport pour un contrôle précis de l’envoi des requêtes ou pour des tests.
Restez fidèle aux demandes si…
- Votre application est purement synchrone et a des besoins HTTP simples. Si Requests fait déjà bien le travail et que vous n’êtes pas confronté à des goulots d’étranglement d’E/S, il n’y a peut-être aucune raison impérieuse de changer.
- Vous travaillez sur une base de code héritée fortement dépendante des requêtes. La migration n’en vaut peut-être pas la peine, sauf si vous avez spécifiquement besoin des fonctionnalités de HTTPX.
- Il est essentiel de minimiser les dépendances. Les requêtes ont une empreinte légèrement inférieure.
- La courbe d’apprentissage d’asyncio est un obstacle pour votre équipe. Bien que HTTPX propose une API de synchronisation, sa principale puissance réside dans ses capacités asynchrones.
Résumé
Mon enquête révèle que HTTPX est une bibliothèque très compétente. Bien qu’il ne fasse pas comme par magie des appels HTTP uniques et synchrones considérablement plus rapides que les requêtes (la latence du réseau y est toujours reine), sa véritable puissance apparaît dans les applications asynchrones. Lors de nombreux appels simultanés liés aux E/S, HTTPX offre des gains de performances substantiels et un moyen plus efficace de structurer le code, comme le démontre notre test simultané.
Beaucoup prétendent que HTTPX est « meilleur », mais cela dépend du contexte. Si « mieux » signifie avoir un support natif asynchrone/attente, des capacités HTTP/2 et une API moderne qui répond également aux besoins synchrones, alors oui, HTTPX détient sans doute un avantage pour les nouveaux développements. Requests reste une bibliothèque excellente et fiable pour les tâches synchrones, et sa simplicité reste sa plus grande force.
Pour simultané opérations asynchrones, le débit efficace lors de l’utilisation de httpx peut être d’un ordre de grandeur supérieur à celui de des requêtes synchrones séquentielles, ce qui change la donne.
Si vous êtes un développeur Python gérant les appels HTTP, en particulier dans les applications Web modernes, les microservices ou les tâches gourmandes en données. Dans ce cas, HTTPX n’est pas simplement une bibliothèque à observer : c’est une bibliothèque à utiliser. La transition depuis les requêtes se fait en douceur pour le code synchrone, et son ensemble global de fonctionnalités et ses prouesses asynchrones en font un choix convaincant pour l’avenir des clients HTTP Python.



