PythonIntermediate 15 min Lesson 24 of 30

Day 24 — Working with APIs

Call a web API from Python properly: status codes, timeouts, retries, authentication headers, and handling failure without crashing.

Python · Lesson 24 of 30
0/30 done(0%)

What is it? #

An API is a way for two programs to talk over HTTP. You send a request to a URL; the other side sends back a response with a status code and usually some JSON.

In Python the common tool is the requests library. It handles connections, encoding and JSON parsing, leaving you to worry about the parts that matter: what you send and what you do when things go wrong.

Things will go wrong. Networks time out, services return 500s, rate limits kick in. Code that calls an API without a timeout and without error handling will eventually hang or crash in production.

The discipline is small and always the same: set a timeout, check the status, handle failures, and never log secrets.

Think of it like this #

Calling an API is like ordering food by phone. You dial a number (the URL), say what you want (the request), and they tell you whether it is coming and how long (the status and response). Sometimes nobody picks up. A sensible person hangs up after a while and tries again later — that is a timeout and a retry.

Simple example #

You need to fetch a user's repositories from a public API, handle the case where the user does not exist, cope with slow responses, and retry transient failures a couple of times.

Code #

PYTHON
import os
import time
import requests

API_BASE = "https://api.github.com"
TOKEN = os.environ.get("GITHUB_TOKEN")        # never hardcode secrets


def get_repos(username: str, retries: int = 2) -> list[dict]:
    url = f"{API_BASE}/users/{username}/repos"
    headers = {"Accept": "application/vnd.github+json"}
    if TOKEN:
        headers["Authorization"] = f"Bearer {TOKEN}"

    for attempt in range(retries + 1):
        try:
            response = requests.get(
                url,
                headers=headers,
                params={"per_page": 5, "sort": "updated"},
                timeout=10,                    # always set a timeout
            )
        except requests.Timeout:
            if attempt == retries:
                raise
            time.sleep(2 ** attempt)           # back off before retrying
            continue

        if response.status_code == 404:
            raise LookupError(f"No such user: {username}")
        if response.status_code == 429:
            wait = int(response.headers.get("Retry-After", 5))
            time.sleep(wait)
            continue
        if response.status_code >= 500 and attempt < retries:
            time.sleep(2 ** attempt)
            continue

        response.raise_for_status()            # raises for any other 4xx/5xx
        return response.json()

    raise RuntimeError("Giving up after retries")


for repo in get_repos("python"):
    print(repo["name"], repo["stargazers_count"])

How it works #

os.environ.get("GITHUB_TOKEN") reads the token from the environment. Secrets belong in environment variables or a secret manager, never in source code — a committed token is a security incident.

params={"per_page": 5} builds the query string safely, escaping values for you. Manual string concatenation into URLs is how injection and encoding bugs start.

timeout=10 is the single most important argument here. Without it, requests waits indefinitely, and one unresponsive service can freeze your whole application.

Status codes are checked deliberately. A 404 is a real answer — the user does not exist — so it becomes a clear LookupError rather than a generic failure. A 429 means you are rate limited, and the Retry-After header tells you how long to wait.

time.sleep(2 ** attempt) is exponential backoff: wait 1 second, then 2, then 4. Retrying immediately in a tight loop makes an overloaded service worse.

response.raise_for_status() turns any remaining error status into an exception, so success is the only way out of the function.

response.json() parses the JSON body into Python lists and dictionaries. From there it is just the data handling you learned on Day 6.

Real-world use #

Almost every application calls something: payment gateways, email providers, maps, SMS, internal microservices. The pattern above — timeout, status handling, bounded retries with backoff — is the baseline everywhere.

Retrying blindly is dangerous for non-idempotent calls. Retrying a GET is safe; retrying a payment charge can bill someone twice. Real payment APIs solve this with idempotency keys, and you should use them when offered.

For many calls in sequence, reuse a requests.Session. It keeps the TCP connection open, which is noticeably faster than opening a new one each time.

On the receiving side, these are the same rules you will apply when building your own API in the System Design and FastAPI lessons: return honest status codes, document rate limits, and send a Retry-After header when you throttle.

Common mistakes #

  • Omitting timeout. The default is to wait forever, which eventually hangs your service.
  • Assuming a 200 response. Always check the status or call raise_for_status().
  • Hardcoding API keys in source. Use environment variables and keep them out of git.
  • Retrying non-idempotent requests without an idempotency key, causing duplicate actions.
  • Logging the full request including headers, which leaks tokens into your logs.

Practice #

Write a function that fetches a public JSON endpoint of your choice with a 5-second timeout, raises a clear error on 404, retries twice on a 500 with backoff, and prints three fields from the response. Then run it with the network disabled and confirm it fails with a readable message rather than a traceback dump.

Quick quiz

  1. 1. Why is `timeout` important on an HTTP request?

  2. 2. What does status code 429 mean?

  3. 3. What is exponential backoff?

  4. 4. Where should an API token live?

  5. 5. Which request is unsafe to retry blindly?

Summary

  • Always set a timeout on outbound HTTP calls.
  • Check status codes deliberately — 404, 429 and 5xx each deserve different handling.
  • Retry with exponential backoff, and only for safe or idempotent operations.
  • Keep secrets in environment variables and out of logs.
  • Use params for query strings and a Session for many calls.