Nine out of ten API calls I write are the same five moves: GET or POST, some parameters, maybe a header, a timeout, and a check that it actually worked. requests makes all five one-liners — and quietly lets you skip the two that matter most. This is the short version of what I actually use.
params=import requests
r = requests.get(
"https://api.github.com/search/repositories",
params={"q": "language:python", "sort": "stars"},
timeout=10,
)
r.raise_for_status()
data = r.json()
print(data["total_count"])
params= builds the query string for you, including fiddly encoding — the colon in language:python becomes language%3Apython on the wire, which is exactly the kind of thing I used to get wrong by hand. r.json() parses the response body into a Python dict. The response object is worth a look around, too: r.text is the raw body, r.headers["content-type"] tells you what you actually got, and r.url shows where your params landed — which is how I debug half of my API surprises.
json= and headers=r = requests.post(
"https://httpbin.org/post",
json={"title": "water the plants", "done": False},
headers={"Authorization": "Bearer ghp_your_token_here"},
timeout=10,
)
json= serializes the dict and sets the Content-Type: application/json header in one move — compare that with data=, which sends old-school form fields, a distinction that cost me an hour once when an API silently ignored my form-encoded payload. headers= is where auth tokens go; every serious API wants one.
timeout=: the hang-forever mistakeHere is the line that cost me a whole night of script runs:
# no timeout= — if the server stalls, this call waits forever
r = requests.get("https://example.com/api")
requests has no default timeout. None. A sync script of mine "ran" less and less often until I realized it wasn't finishing at all — the server had started stalling mid-response, and requests was happy to wait indefinitely, holding a socket open in a process I thought had ended hours ago. With timeout=10 the same situation becomes an exception after ten seconds: annoying, visible, fixable. It's one keyword argument. Put it on every call.
raise_for_status(): the check that isn't automaticr = requests.get("https://api.github.com/repos/python/nope", timeout=10)
print(r.status_code) # 404
r.raise_for_status()
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://api.github.com/repos/python/nope
A 404 is a perfectly normal response as far as requests is concerned — no exception, no complaint. If you don't call raise_for_status(), the failure doesn't vanish; it resurfaces twenty lines later as a KeyError when you dig into the JSON of an error page. So my rule is mechanical: raise_for_status() goes on the line directly under every get and post, like a seatbelt — boring until the day it isn't.
requests is the library I'd design myself, with one exception: timeout should never have been optional. A network call with no timeout isn't patient code — it's a script that can quietly stop working while still running. Between those two defaults, the library's main lesson is that the dangerous parts of HTTP aren't the request; they're the response you didn't check and the socket you didn't close.