lothc¶
Lord Of The Http Clients — a typed HTTP client for Python, built on pyreqwest (an awesome Rust-backed HTTP client), with first-class optional support for pydantic and msgspec as both decode and encode targets.
Installing¶
Any subset of the extras works — the base package alone gets you bytes/dict support.
Quickstart¶
Every snippet below is a complete, runnable script — hit the extra clipboard icon in its
top-right corner to copy a one-liner that runs it via uv run, no local install needed.
import asyncio
from lothc import HTTPClient
from msgspec import Struct
class Pokemon(Struct):
id: int
name: str
async def main() -> None:
async with HTTPClient.build(base_url="https://pokeapi.co/api/v2/") as client:
pikachu = await client.get("pokemon/pikachu", response_data_type=Pokemon)
print(pikachu) # Pokemon(id=25, name='pikachu')
asyncio.run(main())
import asyncio
from lothc import HTTPClient
from pydantic import BaseModel
class Pokemon(BaseModel):
id: int
name: str
async def main() -> None:
async with HTTPClient.build(base_url="https://pokeapi.co/api/v2/") as client:
pikachu = await client.get("pokemon/pikachu", response_data_type=Pokemon)
print(pikachu) # id=25 name='pikachu'
asyncio.run(main())
import asyncio
from lothc import HTTPClient
async def main() -> None:
async with HTTPClient.build(base_url="https://pokeapi.co/api/v2/") as client:
pikachu = await client.get("pokemon/pikachu", response_data_type=dict)
print(pikachu) # {'id': 25, 'name': 'pikachu', ...}
asyncio.run(main())
Warning
response_data_type=dict gets you a plain dict[str, Any] — no schema, no extra
dependency, just parsed JSON. There's no validation at all: the shape is fully trusted,
on your say-so alone.
SyncHTTPClient mirrors every method in these docs one-for-one — swap async with for with,
drop the awaits, and everything still applies.
Why?¶
Why was lothc built, and why should you use it?
pyreqwest is a genuinely excellent HTTP client for Python, fast because the heavy lifting happens
in Rust, not pure Python. But its API is a builder pattern (client.get(path).build() before you
can even .send() it), unfamiliar to anyone coming from requests/httpx/niquests, where a
call is just client.get(url, params=..., headers=...). lothc wraps pyreqwest with exactly that
familiar shape, so you get pyreqwest's speed without giving up the ergonomics Python HTTP users
already expect.
The other reason: Python's most popular third-party HTTP clients (httpx, aiohttp, niquests) all
have first-class JSON support: call .json() and get back a dict. But in order for a Python
project to be end-to-end type-safe, data crossing I/O boundaries must be validated, and if
invalid, handled accordingly (raise an error) — a plain dict does neither. Closing that gap is
exactly what lothc is designed for.
The original idea was to make lothc backend-agnostic: httpx, aiohttp, niquests, and pyreqwest all interchangeable underneath the same typed lothc interface, so switching backends never meant switching your call sites. However, in the author's opinion there's currently no compelling reason not to just use pyreqwest, so pyreqwest is the only backend implemented today, but, if there's demand for an httpx/aiohttp/niquests backend, the author is happy to consider adding one.
Performance¶
lothc is built on top of the awesome pyreqwest package — a Rust-based HTTP client for Python. lothc then adds some nice abstractions on top of that (discussed in the other sections of these docs): typed decode targets, retries, SSE, streaming, and so on. Because of that, there's a bit of overhead compared to using pyreqwest directly — however, this is almost negligible, as can be seen below: the heavy lifting still happens in Rust, so lothc stays far closer to pyreqwest's throughput than to any pure-Python HTTP library's.
Benchmarked with perf.py against a tiny Rust-based static JSON server, 10,000 requests at
concurrency 100 — including lothc's fully-typed decode targets (response_data_type= a msgspec
Struct or a pydantic BaseModel), not just raw bytes or an untyped dict. The object handed back
from those runs isn't just parsed JSON — it's a real, constructed, field-validated instance of
your own type, and that validation cost is included in the numbers, not benchmarked around.
Decoding into a real msgspec Struct even edged out the unvalidated dict path in this run. See
Benchmarks for the full numbers behind the chart above.
Highlights¶
-
Type-safe I/O boundaries, not bolted on
response_data_typegets you a real, constructed, field-validated pydanticBaseModelor msgspecStructstraight from the client, instead of the plaindicta.json()call leaves you to validate yourself. See Benchmarks for what that actually costs (usually nothing). -
Typed decode targets
Pick a pydantic
BaseModel, a msgspecStruct, plaindict, or rawbytes(the default) per call. No cast-laden internals — every verb is built from paired@overloads, so every call site gets a precise static type. -
Typed request bodies too
json=isn't limited to a plaindict— pass a pydanticBaseModelor msgspecStructinstance directly and it's encoded for you. Pydantic and msgspec are decode and encode targets in lothc, not just decode. -
Every verb
get,get_result,post,put,patch,delete,head,download— plussse,stream_get, andstream_postfor streaming responses. See Verbs. -
Typed and raw params/headers
Pass a plain
dict/Mapping, or aBaseModel/Structfor free per-field validation andNone-field omission. -
Streaming, both ways
Raw chunks by default (unbuffered, safe for large binary bodies), or switch to newline- buffered, typed NDJSON-style decoding. See Streaming.
-
SSE support
Decode targets accept a class, a pydantic
TypeAdapter, or a prebuilt msgspecDecoder— so even discriminated-union event streams decode natively. See SSE. -
Retries with real backoff
Implemented as a real pyreqwest
with_middlewarehook. HonorsRetry-After, defaults to the idempotent verbs. See Retries. -
Authentication
A static
bearer_token, orbearer_authfor a token resolved fresh on every request — plusdefault_headersfor anything else that needs to go out on every request. See Authentication. -
Cookies, redirects, proxy
An in-memory cookie jar, redirect control, and proxying. See Cookies, redirects & proxy.
-
A real error hierarchy
HTTPTransportError/HTTPTimeoutError/HTTPConnectionErrorfor failures with no response, and a separateHTTPResponseErrorfor 4xx/5xx. See Error handling.
pydantic and msgspec are both optional — the library works with neither, either, or both installed.