Chapter 4 · Mid-level Developer

Calling an API with requests

An API lets your program fetch data from somewhere else over the web. The everyday tool for this in Python is the requests library — not part of the stdlib, but so standard it may as well be (pip install requests).

requests.get(url) makes an HTTP GET request and hands back a response object. The three things you almost always want from it:

  • .status_code — the HTTP result code. 200 means OK; 404 not found; 401/403 unauthorised; 500 server error.
  • .json() — parses a JSON response body into a Python dict/list (it calls json.loads for you).
  • .text — the raw body as a string, when it isn't JSON.

Query parameters go in the params= argument as a dict — requests builds the ?key=value&... part of the URL for you, correctly escaped. Always check the status before trusting the body: a 404 page is still "a response", just not the data you wanted. response.raise_for_status() turns any 4xx/5xx into an exception in one line.

*(In the Rungo lesson the network is stubbed — a local function returns the same shape of dict — so you write the exact same code against response['status_code'] and response['temp_c']. The real-world API is shown here.)*

Syntax

import requests

r = requests.get("https://api.example.com/weather",
                 params={"city": "London", "units": "metric"})

r.status_code        # 200, 404, ...
r.json()             # parse JSON body → dict / list
r.text               # raw body as a string
r.raise_for_status() # raise if 4xx/5xx, else do nothing

Worked examples

A basic GET with query params

import requests

r = requests.get(
    'https://api.example.com/weather',
    params={'city': 'London', 'units': 'metric'},
)
# requests builds: .../weather?city=London&units=metric

if r.status_code == 200:
    data = r.json()
    print(data['temp_c'])
else:
    print(f'Request failed: {r.status_code}')

raise_for_status — fail fast on a bad response

import requests

r = requests.get('https://api.example.com/data')
r.raise_for_status()   # raises requests.HTTPError on 4xx/5xx
data = r.json()        # only runs if the status was OK
print(data)

Using the parsed dict (the shape the lesson stubs)

# The stub returns a dict shaped like a real .json() body:
response = {'city': 'London', 'temp_c': 18,
            'conditions': 'cloudy', 'status_code': 200}

def summary(response):
    return f"{response['city']}: {response['temp_c']}\u00b0C, {response['conditions']}"

print(summary(response))
# Output:
# London: 18\u00b0C, cloudy

Common mistakes & gotchas

A response is not the same as a success

requests.get returns a response object even for a 404 or 500 — it doesn't raise on its own. If you call .json() and use the data without checking .status_code (or calling .raise_for_status()), you'll happily process an error page as if it were real data.

.json() raises if the body isn't JSON

Calling .json() on an HTML error page or empty body throws a JSONDecodeError. Check the status code first, and reach for .text when you're not sure the response is JSON.

Build params with params=, don't glue the URL

Hand-concatenating url + "?city=" + city breaks on spaces and special characters. Pass params={"city": city} and requests escapes everything correctly. Same lesson as pathlib: let the library build the string.

Why it matters

Almost no useful backend lives alone — it talks to weather services, payment gateways, mapping APIs, other microservices. `requests.get` and reading a status code are the bread and butter of that conversation, and knowing to *check the status before trusting the body* is what separates code that works from code that works until the API hiccups.