Python's requests library has been the default HTTP client for more than a decade, and for good reason: the common case is one line. That convenience hides three decisions you should make on purpose, though: where your data goes in the request, what counts as failure, and whether each call opens a new connection. This lesson makes each one explicit and ends with a helper you can drop into any script.
Everything runs on Python 3.9 or newer with requests installed:
python -m pip install requestsAs in the last lesson, the key lives in an environment variable called APIVERVE_API_KEY, never in the file.
One call, read carefully
import os
import requests
res = requests.get(
"https://api.apiverve.com/v1/dictionary",
params={"word": "retry"},
headers={"x-api-key": os.environ["APIVERVE_API_KEY"]},
timeout=15,
)
body = res.json()
print(body["data"]["definitionCount"], body["data"]["definitions"][0])Compare it with the JavaScript version and the differences are all in your favour:
params=builds and encodes the query string for you. You never write?word=by hand, so a value with&, spaces or+in it arrives intact.headers=is a plain dict. Header names are case-insensitive on the wire, sox-api-keyandX-API-Keyare the same header; lowercase is the convention.os.environ["APIVERVE_API_KEY"]uses square brackets on purpose. If the variable is missing you get aKeyErrornaming it, immediately.os.environ.get(...)would handNoneto requests, which drops a header whose value isNoneentirely, and you would be debugging an edge rejection instead of a missing variable.timeout=15is not optional in spirit.requestshas no default timeout: without one, a stalled connection can hang your script indefinitely. Lesson three goes deeper; for now, always pass it.
res.json() parses the body. Unlike fetch, it is synchronous, because by the time requests.get returns, the whole body has been downloaded.
curl "https://api.apiverve.com/v1/dictionary?word=retry" \
-H "x-api-key: YOUR_API_KEY"data.definitionCount tells you how many entries are in data.definitions.
params versus json
The question beginners ask most about requests is which keyword to put their data in. It depends on where the API expects it, and that is set by the HTTP method on the API's reference page.
| Keyword | Where the data goes | Typical method | Content-Type set for you |
|---|---|---|---|
params= | Query string in the URL | GET | none |
json= | Request body, serialised as JSON | POST | application/json |
data= | Request body, form-encoded (dict) or raw (str/bytes) | POST | application/x-www-form-urlencoded for a dict |
Every API used in this course is a GET, so params= is the answer. You need json= the moment you send a body. APIVerve's batch requests are a real example: one POST that carries many inputs for the same endpoint in a requests array.
import os
import requests
res = requests.post(
"https://api.apiverve.com/v1/emailvalidator/batch",
json={"requests": [
{"email": "[email protected]"},
{"email": "not-an-email"},
]},
headers={"x-api-key": os.environ["APIVERVE_API_KEY"]},
timeout=30,
)
body = res.json()
for item in body["results"]:
print(item["status"], item["error"])Two mistakes show up here constantly:
data=with a dict when the API wants JSON. The body is sent form-encoded (requests=...), the server cannot find the field it expected, and you get a400. If you usedata=json.dumps(payload)instead, you must also setContent-Type: application/jsonyourself.json=does both, which is why it is the right default.params=on a POST, orjson=on a GET. The data is sent somewhere the server is not looking. Because undeclared parameters are dropped silently rather than rejected, a misplaced parameter can produce a successful response to a different question.
An endpoint is documented as POST with a JSON body. Which call sends the body correctly?
Answer the check above to continue.
Where httpx fits
httpx is a newer client with an API deliberately close to requests: httpx.get(url, params=..., headers=..., timeout=...) reads the same. Three differences matter in practice:
- It applies a default timeout (five seconds) instead of none. That is safer, and it also means a slow endpoint that worked with requests can start raising
httpx.ReadTimeout. Set the value you actually want. - It has an async client.
httpx.AsyncClientworks withasyncio, which is the natural fit if your app is already async (FastAPI, for example). Making requests calls from async code blocks the event loop. - The session equivalent is
httpx.Client, used as a context manager so its connections are closed when you are done.
import os
import httpx
with httpx.Client(
base_url="https://api.apiverve.com/v1",
headers={"x-api-key": os.environ["APIVERVE_API_KEY"]},
timeout=15,
) as client:
res = client.get("/numbertowords", params={"number": 1999})
body = res.json()
if res.is_success and body.get("status") == "ok":
print(body["data"]["words"])The envelope check is identical; only the property names differ (res.is_success rather than res.ok). Pick requests for scripts and synchronous code, httpx when you need async or want a timeout by default. Do not mix both in one codebase without a reason.
Before you move on
You now have the same contract in two languages: the key comes from the environment and fails loudly when missing, parameters are encoded by the library, both the HTTP status and the envelope are checked, a non-JSON body is survived, and errors carry their status so callers can make decisions. In Python, a session makes all of that cheaper per call.
What neither helper does yet is cope with a slow or flaky network. The next lesson adds timeouts you control, and retries that only fire when retrying can actually help: Timeouts, retries and backoff.