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 #
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.