Calling APIs Checkpoint
25 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Fix the mistakes
The slips that cost marks in this module. Spot each one, then check the fix.
A reliable API client does four things well: it keeps its key out of the code, it never waits for ever, it retries only what can succeed, and it can be tested without a network. This checkpoint builds one and then tests the module.
The client holds the settings
Put the base URL, the key and the timeout in one client object. Callers then ask for data and do not repeat these details at every call.
import httpx
def build_client(api_key): return httpx.Client( base_url="https://api.shop.example", headers={"X-API-Key": api_key}, timeout=httpx.Timeout(10.0, connect=2.0), )The key comes in as an argument. The program reads it from the environment once, at start-up.
Retry in one small function
Wrap the call in a loop. Retry timeouts and the statuses 429, 502, 503 and 504. Wait with backoff. Return other responses at once, and give up after the last attempt.
RETRY = {429, 502, 503, 504}
def delay(attempt, base=0.5, cap=8.0): return min(cap, base * 2 ** (attempt - 1))
print([delay(a) for a in (1, 2, 3)])Pass the sleep function in. A test can then record the delays and run instantly.
Report failures clearly
When all attempts fail, raise one clear error that names the last status. A 4xx other than 429 is not retried: it raises at once, because the request must be fixed.
class ApiError(Exception): def __init__(self, status): super().__init__(f"API call failed with {status}") self.status = status
print(ApiError(503))Example 1 · A small resilient client
import osimport httpx
RETRY = {429, 502, 503, 504}
class ApiError(Exception): def __init__(self, status): super().__init__(f"API call failed with {status}") self.status = status
class ShopClient: def __init__(self, api_key, transport=None, sleep=lambda s: None, attempts=4): self.sleep = sleep self.attempts = attempts self.http = httpx.Client( base_url="https://api.shop.example", headers={"X-API-Key": api_key}, timeout=httpx.Timeout(10.0, connect=2.0), transport=transport, )
def get(self, path): for attempt in range(1, self.attempts + 1): try: r = self.http.get(path) except httpx.TimeoutException: r = None if r is not None and r.status_code not in RETRY: if r.is_success: return r.json() raise ApiError(r.status_code) if attempt < self.attempts: self.sleep(min(8.0, 0.5 * 2 ** (attempt - 1))) raise ApiError(r.status_code if r is not None else 0)
calls = {"n": 0}
def server(request): calls["n"] += 1 if request.headers.get("x-api-key") != "demo-key": return httpx.Response(401) if request.url.path == "/missing": return httpx.Response(404) if calls["n"] <= 2: return httpx.Response(503) return httpx.Response(200, json={"orders": 5})
os.environ["SHOP_API_KEY"] = "demo-key" # normally set outside the programwaits = []client = ShopClient(os.environ["SHOP_API_KEY"], httpx.MockTransport(server), sleep=waits.append)print(client.get("/orders"), "after", calls["n"], "calls; waited", waits)
for path, key in (("/missing", "demo-key"), ("/orders", "wrong")): try: ShopClient(key, httpx.MockTransport(server)).get(path) except ApiError as err: print(path, "->", err){'orders': 5} after 3 calls; waited [0.5, 1.0]
/missing -> API call failed with 404
/orders -> API call failed with 401What this code does
- The client retries the two 503 replies with waits of 0.5 and 1.0 seconds, then returns the data.
- A 404 and a 401 are not retried; they raise
ApiErrorat once.
Example 2 · Which failures are worth a retry?
import httpx
RETRY = {429, 502, 503, 504}
def decide(outcome): if isinstance(outcome, httpx.TimeoutException): return "retry (timeout)" if outcome.status_code in RETRY: return "retry" return "ok" if outcome.is_success else "report"
cases = [ httpx.ReadTimeout("slow"), httpx.Response(200), httpx.Response(503), httpx.Response(404), httpx.Response(401),]for case in cases: label = type(case).__name__ if isinstance(case, Exception) else case.status_code print(label, "->", decide(case))ReadTimeout -> retry (timeout) 200 -> ok 503 -> retry 404 -> report 401 -> report
What this code does
- A timeout and the busy-server codes may pass on the next try.
- A 404 or 401 means the request must be fixed, so it is reported at once.
Callers catch ApiError and decide what to show the user.
Finish up
Mark this lesson complete
This is the last lesson in this section.