Frameworks reference

APIs & Datasets Reference

HTTP, JSON, authentication, pagination and rate limits, then finding, cleaning and joining real public data without misrepresenting it.

HTTP

Every API request is an HTTP request. Knowing the handful of pieces it is made of turns most API problems from mysteries into something you can read off the response.

The shape of a request

A method, an address, some headers, and sometimes a body.

GET /search?q=kindred&page=2 HTTP/1.1
Host: api.example.org
Accept: application/json
Authorization: Bearer abc123
  • The method says what kind of operation this is.
  • Everything after the ? is the query string: parameters, joined by &.
  • Headers carry metadata: what you accept, who you are, what format you are sending.

Methods

Five you will use, and one property that matters more than the names.

GET     read something, changing nothing
POST    create something
PUT     replace something entirely
PATCH   change part of something
DELETE  remove something
  • GET must be safe: it must not change anything. Browsers and crawlers prefetch GETs.
  • GET, PUT and DELETE are idempotent: doing them twice is the same as doing them once.
  • POST is not, which is why a refresh after a POST can create a duplicate.

Status codes

The first digit tells you who has the problem, which is usually all you need.

2xx  it worked
3xx  it moved
4xx  you got something wrong
5xx  the server got something wrong

200  OK
201  Created
204  No content
301  Moved permanently
400  Bad request
401  Not authenticated
403  Authenticated, but not allowed
404  Not found
409  Conflict
422  Understood, but invalid
429  Too many requests
500  Server error
503  Temporarily unavailable
  • 401 versus 403 is the useful distinction: 401 means we do not know who you are, 403 means we do and the answer is still no.
  • 429 means slow down, and the response usually says by how much.
  • Retry 5xx and 429. Do not retry 4xx: nothing about the request will improve on its own.

Query parameters

Options on the end of a URL. Anything a person typed must be encoded.

# wrong: a space or an ampersand breaks the URL
url = "https://api.org/search?q=" + query

# right
from urllib.parse import urlencode
url = "https://api.org/search?" + urlencode({"q": query, "page": 2})
  • JavaScript: encodeURIComponent(value), or build it with URLSearchParams.
  • An unencoded & in a search term silently becomes a second parameter.
  • Never put a secret in a query string. URLs are logged by everything they pass through.

REST and API shapes

What REST means in practice

Addresses name things, and methods say what to do with them. It is a convention rather than a standard, and most APIs follow it loosely.

GET    /posts          list them
POST   /posts          create one
GET    /posts/42       read one
PATCH  /posts/42       change part of it
DELETE /posts/42       remove it

GET    /posts/42/comments   things belonging to it
  • Nouns in the path, verbs as the method. /getPosts is a sign the API was designed by accident.
  • Plural for collections, an identifier for one member. Consistency matters more than which convention.

Reading the documentation

The five things worth finding before writing any code, in this order.

  • The base URL, and whether there is a version in it.
  • Whether it needs authentication, and of what kind.
  • The rate limit, and what happens when you exceed it.
  • How pagination works, because every API does it differently.
  • What an error looks like, so you can tell one from real data.
  • Then make one request by hand with curl before writing a single line of code.

Trying a request by hand

Before automating anything, see the actual response once.

curl "https://api.example.org/search?q=kindred"

curl -i "https://api.example.org/search?q=kindred"

curl -H "Authorization: Bearer $TOKEN" \
     "https://api.example.org/me"
  • -i shows the status line and headers, which is where the rate limit and paging hints live.
  • Piping into a formatter makes a wall of JSON readable: curl … | python -m json.tool
  • Half of API debugging is discovering the response is not the shape you assumed.

JSON

The whole format

Six types, and that is genuinely all of it.

{
  "title": "Kindred",       // string
  "year": 1979,             // number
  "published": true,        // boolean
  "sequel": null,           // null
  "tags": ["novel"],        // array
  "author": { "name": "" }  // object
}
  • No comments, no trailing commas, no single quotes. Keys are always double-quoted strings.
  • No date type. Dates arrive as strings, usually ISO 8601, and you convert them yourself.
  • No integer versus float distinction. Very large numbers can lose precision, so ids are often sent as strings.

Parsing safely

Parse, then check, then use. Skipping the middle step is where most crashes come from.

import json

data = json.loads(text)

# a missing key raises; a default does not
title = data.get("title", "")

# nested access, safely
name = (data.get("author") or {}).get("name", "")

# a list that might be absent
for tag in data.get("tags") or []:
    ...
  • JavaScript: JSON.parse(text), then optional chaining, data?.author?.name ?? "".
  • Real responses have missing fields, nulls where you expected objects, and empty arrays.
  • A field being present in one response does not mean it is present in all of them.

Shape surprises worth expecting

Things real APIs do that tutorials do not mention.

  • A field is sometimes a string and sometimes a number, in the same endpoint.
  • A single result is returned as an object, and multiple results as an array.
  • null and an empty string and a missing key all mean 'no value', inconsistently.
  • An error is returned with status 200 and an error key in the body. Always check the body, not only the status.

Authentication and secrets

The common kinds

Four you will meet, in rough order of how often.

# no auth at all: many public archives
curl "https://api.example.org/items"

# key in a header (preferred)
curl -H "X-API-Key: $KEY" …

# bearer token
curl -H "Authorization: Bearer $TOKEN" …

# key in the query string (avoid where possible)
curl "https://api.example.org/items?key=$KEY"
  • A header keeps the secret out of logs, browser history and referrer headers. A query string does not.
  • OAuth is the fourth: a flow that gets you a token, on behalf of a user. Worth learning when you need it, not before.

Where a key must not go

The rules are short and the consequences are not.

# never
KEY = "sk_live_abc123"

# read it from the environment
import os
KEY = os.environ["API_KEY"]
  • Not in source code. Not in git. Not in frontend JavaScript, where every visitor can read it.
  • Add the file holding it to .gitignore before creating the file, not after.
  • If a key reaches a public repository, rotate it. Deleting the commit does not help: the history is public.
  • Frontend code cannot hold a secret at all. If a browser must reach the API, route it through a server you control.

Pagination

No API returns a million rows at once. Every one of them splits the results, and they all do it slightly differently.

The three styles

Page numbers, offsets, or cursors.

?page=2&per_page=100          page numbers
?offset=100&limit=100         offset
?cursor=eyJpZCI6MTAwfQ        cursor / token
  • Page and offset are easy to reason about and can skip or repeat rows when the data changes mid-crawl.
  • Cursors are stable across changes, and you cannot jump to an arbitrary page.
  • With a cursor, you keep following the next value until it is absent. That absence is the stop signal.

Looping through every page

The shape, with the stop condition made explicit.

results = []
page = 1

while True:
    data = fetch(page)
    batch = data.get("results", [])
    if not batch:
        break
    results.extend(batch)
    if not data.get("next"):
        break
    page += 1
    if page > 500:            # a hard stop, always
        break
  • Always have a maximum. A paging bug that never terminates will hammer someone else's server.
  • Stop on an empty batch as well as on a missing next link. Some APIs give you one and not the other.
  • Save each page as you go for anything large, so a failure at page 400 does not cost you the first 399.

Total counts are often approximate

A count field is frequently an estimate, and may not match what you actually receive.

  • Do not assert that you collected exactly the number the API claimed. Report both if they differ.
  • Deduplicate by id after collecting. Page-based paging over changing data really does return duplicates.

Rate limits and being a good client

Reading the limit

Most APIs tell you your budget in the response headers, on every response.

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 4
X-RateLimit-Reset: 1735689600

# on a 429
Retry-After: 30
  • Read Retry-After and honour it. It is the server telling you exactly what it wants.
  • Slow down before you hit zero rather than after being refused.

Backing off

When something fails for a reason that might pass, wait longer each time.

import time

delay = 1
for attempt in range(5):
    status = do_request()
    if status < 400:
        break
    if 400 <= status < 500 and status != 429:
        break                 # your fault; retrying will not help
    time.sleep(delay)
    delay *= 2                # 1, 2, 4, 8, 16
  • Retry on 429 and 5xx. Do not retry on 400, 401, 403 or 404.
  • Doubling the wait is exponential backoff. Adding a little randomness stops many clients retrying in lockstep.
  • Cap the number of attempts. Retrying forever turns a small outage into your own denial of service.

Courtesy that costs you nothing

You are a guest on someone else's infrastructure, usually for free.

  • Send a User-Agent identifying your project and a way to contact you. Some archives require it.
  • Cache anything you fetch more than once. The fastest and kindest request is the one you do not make.
  • Request only the fields and the date range you need.
  • Run a crawl once and save the raw responses. Re-run your analysis against those files, not the API.

Errors and reliability

Failures to expect

Networks are unreliable in specific, repeatable ways.

  • A request can hang forever. Always set a timeout: requests.get(url, timeout=10).
  • A response can be truncated, so JSON parsing fails on data that was fine at the source.
  • A 200 can carry an error in the body. Check the body's own success or error field.
  • The same request can succeed and then fail a minute later. That is normal, not a bug in your code.

Failing loudly enough

Silence is the expensive failure mode when you are collecting data.

# quietly wrong: missing pages become missing rows
try:
    data = fetch(page)
except Exception:
    data = {"results": []}

# honest: record what failed and how much
try:
    data = fetch(page)
except Exception as e:
    failures.append({"page": page, "error": str(e)})
    continue
  • A swallowed error becomes a gap in your data with no record that it exists.
  • Report the count you collected and the count you failed to collect, together, every time.
  • An analysis missing eight percent of its rows is not wrong by eight percent. It may be wrong in one direction entirely.

Finding datasets

Where to look

Public data exists in far more places than people expect.

  • Government portals: national, state and city open data sites.
  • International bodies: the World Bank, the UN, WHO.
  • Cultural archives: libraries, museums and universities, many with real APIs.
  • Research repositories, which usually carry the best documentation you will find anywhere.
  • Journalism outlets that publish the data behind their stories.

Judging a dataset before using it

Five questions, before you write any code against it.

  • Who collected it, and for what purpose? Purpose shapes what got measured.
  • When, and what period does it cover? Data with no date is data you cannot cite.
  • How was it collected? Self-reported, observed, and modelled are different kinds of number.
  • Who or what is missing? Absence is usually a decision, not an accident.
  • What is the licence? This decides whether you may publish anything derived from it.

What absence means

The people missing from a dataset are rarely missing at random, and this matters most for exactly the questions this platform exists to ask.

  • Historical records often undercount the people who were least well served by the institution keeping them.
  • A category that did not exist when data was collected produces a gap, not a zero.
  • Aggregating small groups into 'other' erases them from every analysis downstream.
  • State this in your writeup. A finding that names its gaps is stronger than one that does not, not weaker.

CSV and tabular data

Why CSV is harder than it looks

It is the most common format and the least specified one.

import csv

with open("data.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        print(row["name"])
  • Use a CSV library. Splitting on commas breaks on the first quoted field containing a comma.
  • The separator is not always a comma: semicolons and tabs are common.
  • Encoding is frequently not UTF-8. A file full of strange characters is usually latin-1 or cp1252.
  • newline="" on the open call is not optional in Python; without it, quoted line breaks corrupt.

Types arrive as text

Every CSV value is a string until you convert it, and conversion is where the surprises are.

"1,234"      thousands separator
"1.234"      or a decimal comma, depending on locale
"12%"        a unit inside the value
"$1,234"     a currency symbol
"N/A"        not a number
""           empty, meaning unknown
"2026-08-15" a date, as text
  • Convert deliberately and decide what an unconvertible value becomes.
  • Leading zeros disappear the moment an identifier is treated as a number. Postcodes and ids are text.
  • Sorting numbers as text puts 10 before 9. If a chart looks wrong, check this first.

Cleaning

The usual problems

Most cleaning is these six, in roughly this order.

  • Whitespace, including non-breaking spaces that look identical to normal ones.
  • Inconsistent case: NY, ny and New York as three separate categories.
  • Several spellings of the same thing, which is the hardest one to fix correctly.
  • Dates in mixed formats, where 03/04 is ambiguous without knowing the source's country.
  • Duplicates, exact and near.
  • Missing values, represented half a dozen ways in the same column.

Missing is not zero

The single most consequential cleaning decision, and the one most often made by accident.

# these are all "missing" in real files
None, "", "N/A", "NA", "-", "null", "unknown", 0, -1, 999
  • Replacing missing with 0 changes every average you compute afterwards.
  • Dropping rows with missing values silently changes who is in your analysis.
  • Decide per column, write down what you decided, and report how many were affected.
  • 0 and -1 and 999 as missing markers are real and common. Look at the distribution before trusting a number.

Keep the raw file

Never clean in place. Read raw, write clean, keep both.

data/
  raw/        exactly as downloaded, never edited
  clean/      produced by a script
  clean.py    the script, in version control
  • If cleaning is a script, it can be re-run, reviewed and corrected. If it was done by hand, it cannot.
  • Record where each raw file came from and when you fetched it. You will need it for the writeup.

Joining and aggregating

Joining on a key

Combining two datasets on a shared identifier, and the ways it goes wrong.

by_id = {row["id"]: row for row in reference}

joined = []
missing = 0
for row in main:
    match = by_id.get(row["id"])
    if match is None:
        missing += 1
        continue
    joined.append({**row, **match})

print(f"joined {len(joined)}, unmatched {missing}")
  • Always count what did not match. A join that silently drops rows is the classic invisible error.
  • Keys must be the same type. The number 42 and the string "42" do not match.
  • Joining on a name rather than an id fails on spelling, case and punctuation.
  • A duplicate key on one side multiplies rows on the other. Check for duplicates before joining.

Aggregating

Grouping rows and summarising each group.

totals = {}
for row in rows:
    key = row["category"]
    totals[key] = totals.get(key, 0) + row["amount"]
  • Report the group sizes alongside the summary. A mean over three rows is not a finding.
  • A mean is pulled by outliers; a median is not. Which is right depends on the question.
  • Percentages need their denominator stated, every time.
  • Counting rows is not counting people if one person can appear more than once.

Licensing, provenance and honesty

This is part of the work, not an afterthought. A number you cannot source is a number you cannot publish.

Licences you will meet

What you may actually do with what you downloaded.

  • Public domain or CC0: use freely, attribution still courteous.
  • CC BY: use freely, attribution required.
  • CC BY-SA: attribution required, and derived work must carry the same licence.
  • CC BY-NC: no commercial use, and the definition is broader than people assume.
  • No licence stated does NOT mean free to use. It means you have no permission and should ask.
  • Terms of service can restrict scraping and redistribution regardless of the licence on the data.

Provenance to record

Write this down while you have it, because reconstructing it later is miserable.

source:     name of the publisher
url:        exact address you fetched
fetched:    the date you fetched it
coverage:   what period and population it covers
licence:    and a link to it
notes:      anything odd you noticed
  • Datasets get revised and removed. The version you used may not be the one there next year.
  • Recording the fetch date is what lets someone else understand a discrepancy rather than assume you erred.

Stating limits

Every honest piece of data work says what it cannot show.

  • What the data does not cover, and who it leaves out.
  • What you dropped during cleaning, and how much.
  • Which numbers are estimates rather than counts.
  • Correlation is not causation, and a chart placing two lines together implies a claim whether you make it or not.
  • A finding with its limits stated is more credible, not less. Readers who spot an unstated limit stop trusting everything else.

Common errors, and what they mean

401 Unauthorized

The API does not know who you are.

  • A missing, malformed or expired key or token.
  • Check the header name exactly, and that the environment variable actually loaded.

403 Forbidden

It knows who you are, and the answer is still no.

  • A key without the right scope, or a resource that is not yours. Do not retry it.

429 Too Many Requests

You are going too fast.

  • Read Retry-After, wait that long, then continue. Back off rather than hammering.

JSONDecodeError / Unexpected token

What came back was not JSON.

print(response.status_code)
print(response.text[:300])
  • Usually an HTML error page, a rate-limit notice, or a login redirect.
  • Print the first few hundred characters. The answer is almost always visible immediately.

UnicodeDecodeError

The file is not the encoding you assumed.

open(path, encoding="utf-8")
open(path, encoding="latin-1")
open(path, encoding="utf-8-sig")   # strips a byte order mark
  • A first column named id is a byte order mark. utf-8-sig removes it.

Numbers that will not add up

They are strings, or they carry a symbol.

  • "1,234" and "$5" and "12%" are all text. Strip, then convert, and decide what happens when conversion fails.

A join that lost most of its rows

The keys do not match as literally as you think.

  • Different types, stray whitespace, different case, or an id that gained a leading zero.
  • Print a handful from each side and compare them character by character.

Want to learn how to build with this?

A reference tells you what exists. The course teaches you when to reach for it, and has you build something real while you learn.