PythonWorking with APIs

Working with APIs

An API (Application Programming Interface) is a contract that lets two programs talk to each other. When people say "calling an API" in a web context, they usually mean a REST API: a server exposes a set of URLs (endpoints), and a client sends HTTP requests to those URLs to read or change data. The server responds with structured data — almost always JSON these days — that the client parses and uses.

The building blocks
  • Client and server — your Python script is the client; the API is the server. The client initiates every request.

  • Endpoints — URLs that represent a resource, e.g. /users/42 or /orders.

  • HTTP verbsGET reads data, POST creates something new, PUT/PATCH update, DELETE removes.

  • JSON payloads — the request body (for POST/PUT) and the response body are usually JSON: text that maps directly onto Python dicts and lists.

  • Status codes — the response tells you what happened: 200 OK, 201 Created, 401 Unauthorized, 404 Not Found, 429 Too Many Requests, 500 Server Error.

Authentication patterns

Most real APIs require you to prove who you are before they hand back data. The exact mechanism varies, but you will run into these three patterns constantly.

Method

How it works

Example

API key

A static token issued when you sign up, sent as a query param or header on every request.

requests.get(url, params={"api_key": "abc123"})

Bearer token

A token (often short-lived) sent in the Authorization header, prefixed with the word "Bearer".

headers={"Authorization": "Bearer eyJhbGci..."}

OAuth 2.0

A multi-step handshake: the user grants your app permission, your app exchanges a code for an access token, then uses that token like a bearer token.

Used by "Sign in with Google/GitHub" flows

Never hardcode secrets in source code
API keys and tokens should never be typed directly into a `.py` file, especially one that gets committed to git. Anyone with read access to the repository (or its history) gets your credentials. Load them from environment variables instead — see the separate Environment Variables page for how to do this with `os.environ` and a `.env` file.
A full worked example

Here is a complete example calling a public joke API, checking the response, and turning the JSON into a plain Python object.

joke_client.py

Python
import os
import requests

API_KEY = os.getenv("JOKE_API_KEY")  # loaded from the environment, never hardcoded


def fetch_joke():
    response = requests.get(
        "https://official-joke-api.appspot.com/random_joke",
        headers={"Authorization": f"Bearer {API_KEY}"} if API_KEY else {},
        timeout=5,
    )
    response.raise_for_status()
    return response.json()


if __name__ == "__main__":
    joke = fetch_joke()
    print(joke["setup"])
    print(joke["punchline"])

The response comes back as a JSON object like {"setup": "...", "punchline": "...", "id": 1}. Calling .json() turns that straight into a Python dict, so joke["setup"] is just normal dictionary access — no manual parsing required.

Handling rate limits and transient errors

APIs protect themselves from abuse with rate limits — if you call too often, you get back a 429 Too Many Requests instead of your data. Well-behaved clients check for this and back off rather than hammering the server in a tight loop.

retry_with_backoff.py

Python
import time
import requests


def get_with_retry(url, max_attempts=5, timeout=5):
    for attempt in range(1, max_attempts + 1):
        response = requests.get(url, timeout=timeout)

        if response.status_code == 429:
            # respect Retry-After if the server sends one, else back off
            wait = int(response.headers.get("Retry-After", 2 ** attempt))
            print(f"Rate limited, waiting {wait}s (attempt {attempt})")
            time.sleep(wait)
            continue

        response.raise_for_status()
        return response.json()

    raise RuntimeError("Exceeded retry attempts")
Exponential backoff
Doubling the wait time on each retry (`2 ** attempt`) spreads retries out so you don't immediately get rate-limited again. Many HTTP client libraries (and `requests` via the `urllib3.Retry` adapter) can do this for you automatically.
Graceful error handling
  • 401 Unauthorized / 403 Forbidden — your credentials are missing, wrong, or lack permission. Do not silently retry these.

  • 404 Not Found — the endpoint or resource id does not exist. Usually a bug in the URL you built.

  • 429 Too Many Requests — back off and retry, ideally honoring a Retry-After header.

  • 5xx — the problem is on the server. A short retry with backoff is reasonable; if it keeps failing, surface the error.

Keeping keys out of your code

The JOKE_API_KEY in the example above is read with os.getenv(), which returns None if the variable is not set rather than crashing. In development you would put real values in a local .env file (never committed to git) and load them with the python-dotenv package; in production the same variable is set by your hosting platform. This is covered in depth on the Environment Variables page.

Read the docs for the specific API
Every API has its own quirks — pagination style, exact auth header format, rate limit thresholds. This page covers the concepts that transfer between all of them; always check the API's own documentation for the specifics before integrating.