httpx 0.28: requests с человеческим лицом и HTTP/2
requests — стандарт с 2011 года, и он отличный. Но он устарел в нескольких местах: только HTTP/1.1, не поддерживает asyncio, имеет странный дизайн дефолтного timeout (его нет — запрос может висеть вечно). httpx — современная альтернатива от Encode (создатели Starlette и FastAPI), запущена в 2019. Слоган: “next-generation HTTP client”. Версия 0.28 (актуальная на 2026) — production-ready.
В этом уроке разберём sync API httpx (async будет в следующем уроке), посмотрим что он делает иначе чем requests, и решим вопрос “переходить ли с requests на httpx или нет”. Spoiler: переходить не обязательно, но знать httpx — обязательно, потому что половина новых проектов уже на нём.
Установка
$ pip install httpx==0.28.0
# С поддержкой HTTP/2 (опционально, тянет дополнительные C-зависимости через h2)
$ pip install "httpx[http2]==0.28.0"
>>> import httpx
>>> httpx.__version__
'0.28.0'
Базовый API — почти как requests
import httpx
# Top-level функции (как requests)
r = httpx.get("https://api.example.com/users/1")
r = httpx.post("https://api.example.com/users", json={"name": "Alice"})
r = httpx.put("https://api.example.com/users/1", json={"name": "Alice"})
r = httpx.delete("https://api.example.com/users/1")
# Response API -- почти идентичен requests
r.status_code
r.text
r.content
r.json()
r.headers
r.url
Если ты пришёл с requests — 90 процентов кода скопируется без изменений. Под капотом другая реализация, но API близкий.
httpx.Client() — как Session, только лучше
httpx.Client() — аналог requests.Session(). Connection pool, persistent connections, общие настройки.
import httpx
with httpx.Client(
headers={"User-Agent": "MyETL/1.0", "Authorization": "Bearer ..."},
base_url="https://api.example.com",
timeout=30.0,
) as client:
r = client.get("/users/1") # request to https://api.example.com/users/1
r = client.get("/users", params={"role": "admin"})
Что лучше чем у requests:
1. base_url
requests требует полные URL в каждом запросе. httpx позволяет задать base_url= на клиенте и шлёт относительные пути. Это избавляет от string concat-ов:
# requests
session = requests.Session()
BASE = "https://api.example.com"
r = session.get(f"{BASE}/users/1")
r = session.get(f"{BASE}/orders")
# ...
# httpx
with httpx.Client(base_url="https://api.example.com") as c:
r = c.get("/users/1")
r = c.get("/orders")
2. Timeouts — везде по умолчанию
В requests timeout не задаётся по умолчанию — запрос может висеть бесконечно (это часть, за которую разработчики ругают requests). В httpx есть дефолт — 5 секунд:
# httpx -- дефолтный timeout 5 секунд
r = httpx.get("https://slow.example.com") # бросит TimeoutException через 5с
# requests -- без явного timeout
r = requests.get("https://slow.example.com") # будет висеть пока сервер не закроет
И httpx позволяет настраивать каждую фазу отдельно:
import httpx
# Tuple-based -- четыре фазы
timeout = httpx.Timeout(
connect=5.0, # время на TCP+TLS handshake
read=30.0, # время на чтение ответа
write=10.0, # время на отправку body
pool=5.0, # время на получение соединения из pool
)
with httpx.Client(timeout=timeout) as c:
r = c.get("https://api.example.com/users")
# Простой: один timeout на все фазы
with httpx.Client(timeout=30.0) as c:
r = c.get("...")
# Отключить timeout совсем (опасно в production)
with httpx.Client(timeout=None) as c:
r = c.get("...")
Для DE pipeline-ов разделяй timeout-ы: connect короткий (5с — сервер либо ответит, либо нет), read длинный (30-300с — большой JSON может качаться долго). Это даёт быструю reaction на сетевые проблемы и одновременно терпение к медленным запросам.
HTTP/3 и QUIC: следующий шаг после HTTP/2
HTTP/2 — реальный плюс над requests
requests поддерживает только HTTP/1.1. httpx с http2=True использует HTTP/2 если сервер согласен (ALPN negotiation в TLS):
import httpx
with httpx.Client(http2=True) as c:
r = c.get("https://www.google.com/")
print(r.http_version) # 'HTTP/2'
Что даёт HTTP/2:
- Multiplexing: много запросов параллельно через одно TCP-соединение. У HTTP/1.1 — последовательно или несколько TCP параллельно (max 6 на хост в браузерах).
- Header compression (HPACK): повторяющиеся headers (User-Agent, Cookie, Authorization) сжимаются. Экономия трафика 5-15 процентов.
- Binary framing: более эффективный парсинг.
Реальная разница на 100 параллельных запросах к одному API — не x10 как иногда обещают маркетологи, но 1.3-1.5x в типичных случаях. Если API не поддерживает HTTP/2 (многие internal API на nginx без настройки — не поддерживают), httpx fallback на HTTP/1.1 прозрачно.
import httpx
with httpx.Client(http2=True) as c:
r = c.get("https://api.example.com/...")
if r.http_version == "HTTP/2":
print("Используем HTTP/2")
else:
print(f"Fallback на {r.http_version}")
Proxy: стандартизированный конфиг
requests конфигурирует прокси через dict {"http": "...", "https": "..."} — рудимент urllib. httpx сделал нормальный API:
import httpx
# Один прокси на всё
with httpx.Client(proxy="http://user:[email protected]:8080") as c:
r = c.get("https://api.example.com/...")
# Разные прокси по схеме
proxies = {
"http://": "http://proxy1.corp.com:8080",
"https://": "http://proxy2.corp.com:8080",
}
with httpx.Client(mounts={k: httpx.HTTPTransport(proxy=v) for k, v in proxies.items()}) as c:
r = c.get("https://api.example.com/...")
В корпоративных средах с обязательным прокси (PaCO, Squid) — это спасение.
raise_for_status — chainable
В requests r.raise_for_status() возвращает None. В httpx — возвращает self, можно делать chain:
# requests
r = requests.get("...")
r.raise_for_status()
data = r.json()
# httpx -- короче
data = httpx.get("...").raise_for_status().json()
# Или внутри chain
print(httpx.get("https://api.example.com/users/1").raise_for_status().json()["name"])
Микро-удобство, но приятно.
Сравнение API: requests vs httpx
| Что | requests | httpx |
|---|---|---|
| Top-level get/post | requests.get(url) | httpx.get(url) |
| Session/Client | requests.Session() | httpx.Client() |
| Base URL | нет (manual concat) | base_url= |
| Default timeout | None (висит вечно) | 5 секунд |
| Tuple timeout | (connect, read) | httpx.Timeout(connect, read, write, pool) |
| HTTP/2 | нет | да (http2=True) |
| Async support | нет | да (httpx.AsyncClient) |
| Type hints | partial (stubs) | native PEP 484 |
| raise_for_status | returns None | returns self (chainable) |
| Proxy config | proxies={} dict | proxy= или mounts= |
| Cookies API | RequestsCookieJar | httpx.Cookies |
| Hooks | через session.hooks | через event_hooks={...} |
| Streaming | iter_content | iter_bytes, iter_text, iter_lines |
| Default User-Agent | python-requests/2.34 | python-httpx/0.28 |
Hooks в httpx
Похоже на requests, но через event_hooks параметр:
import httpx
def log_request(request):
print(f"-> {request.method} {request.url}")
def log_response(response):
print(f"<- {response.status_code} {response.url}")
with httpx.Client(
event_hooks={
"request": [log_request],
"response": [log_response],
}
) as c:
r = c.get("https://api.example.com/...")
Заметь — hooks принимают список callable. И есть отдельные хуки на request (до отправки) и response (после получения).
Когда httpx vs когда requests
Выбирай httpx если:
- Нужен async/await (FastAPI, asyncio-pipeline).
- Хочешь HTTP/2.
- Нужен base_url, нативные type hints, лучшие timeouts.
- Начинаешь новый проект — там нет смысла тащить requests.
Оставайся на requests если:
- Существующий код, который работает — миграция не оправдана.
- Команда привыкла к requests, нет необходимости перестраиваться.
- Зависимость от specific requests-плагина (есть auth plugins для AWS, OAuth, которые не портированы на httpx).
Оба знай:
- В indústrии 60 процентов кода ещё на requests, 40 процентов уже на httpx.
- httpx читается легче (modern Python), но requests — стандарт legacy.
httpx не пытается быть “ещё одной библиотекой”. Авторы прямо пишут в README: “httpx это эволюция requests для современной экосистемы Python (asyncio, HTTP/2, type hints)”. Если завтра requests добавит async и HTTP/2 — конкуренция исчезнет. Но requests этого почти не делает (последние major-релизы — багфиксы).
Попробуй сам
import httpx
import time
# 1. Basic GET с timeout-ом
with httpx.Client(timeout=10.0) as c:
r = c.get("https://jsonplaceholder.typicode.com/users/1")
r.raise_for_status()
print(r.json())
# 2. Base URL -- без копипасты
with httpx.Client(
base_url="https://jsonplaceholder.typicode.com",
timeout=10.0,
) as c:
users = c.get("/users").raise_for_status().json()
posts = c.get("/posts").raise_for_status().json()
print(f"Users: {len(users)}, Posts: {len(posts)}")
# 3. Сравнение: HTTP/1.1 vs HTTP/2 latency на одной странице
# Не каждый сервер поддерживает HTTP/2, но Google -- да
for use_h2 in [False, True]:
start = time.time()
with httpx.Client(http2=use_h2) as c:
for _ in range(20):
r = c.get("https://www.google.com/")
print(f"http2={use_h2}: {time.time() - start:.2f}s, version={r.http_version}")
# 4. Структурированные timeouts
timeout = httpx.Timeout(connect=2.0, read=30.0, write=5.0, pool=5.0)
with httpx.Client(timeout=timeout) as c:
try:
r = c.get("https://httpbin.org/delay/35") # ждёт 35 секунд
except httpx.ReadTimeout:
print("Read timeout (но не connect -- сервер отвечает быстро)")
Перепиши свой ETL-скрипт с requests на httpx. Заметь разницу в количестве кода — особенно если у тебя был большой BASE_URL = "..." и f"{BASE_URL}/..." везде.
Killer takeaway
httpx 0.28 — современная альтернатива requests от создателей FastAPI. API близок к requests (большинство кода скопируется), но: дефолтный timeout 5с (requests = бесконечный), structured timeouts (connect/read/write/pool отдельно), base_url у Client, HTTP/2 через http2=True, native type hints, async API в одном пакете. httpx.Client() = requests.Session() с лучшим API. HTTP/2 даёт реальный 1.3-1.5x ускорение на параллельных запросах если сервер поддерживает. Если начинаешь новый проект — выбирай httpx. Если есть работающий код на requests — миграция не оправдана. Знай оба: 60/40 в индустрии.