← Teacher guide

Ask an API

An optional lesson. Students ask the Open-Meteo weather API for a week's forecast with requests, read the nested JSON, fall back to a saved copy when there is no connection, then find the hottest day, add rain and compare two places.

Data detective · Dashboards and live data · about 45 minutes · Open the student lesson

Classroom resources

Load these into Waypoints Prepare: choose Load URL and paste the copied address, or download the file and import it. In Prepare, the worksheet's Teacher edition shows the answers; the student copy hides them.

Students will be able to

Timing

Predict and Run 10 min · Investigate 10 min · Modify 15 min · Make 10 min

Before class

Optional: not needed for completing the course. It needs api.open-meteo.com to be reachable from school devices (the Check your school page tests it); if it is blocked, except requests.RequestException: loads weather.json, an example reply in Open-Meteo's format (made up for these lessons, not a real forecast), so the lesson still works. Open-Meteo is free for non-commercial use with credit (CC BY 4.0), needs no key, and allows requests from web pages. In the playground, requests is a small stand-in (get, params, .json(), .status_code, .ok, .raise_for_status()) so the same code runs with the real requests on a computer. The reply is a dictionary; reply["daily"] holds lists (time, temperature_2m_max, ...) where place i in each list is the same day (the bug task skips the daily level). params= builds the query string, the other side of Flask's request.args.

Running the lesson

  1. Predict: draw the nested reply as boxes inside boxes.
  2. Run: find your school's latitude and longitude on a map together.
  3. Investigate: turn off the network (or use a wrong address) and run: the saved copy takes over.
  4. Info: discuss data licences and API keys.

The hard part (on purpose)

Reading nested JSON. Ask students to print reply, then reply["daily"], one level at a time, instead of telling them the path.

When students get stuck

What you will seeA question to askThe fix
KeyError: 'temperature_2m_max'.Which dictionary holds the temperatures?reply["daily"]["temperature_2m_max"].
The forecast is for the wrong place.Is latitude negative south of the equator?Canberra is latitude -35.28, longitude 149.13.
No connection: using the saved forecast, every time.Does the Check your school page reach Open-Meteo on this device?The site is blocked here; the saved copy is fine for the lesson, or ask IT to allow api.open-meteo.com.

Answers to the checks

  1. Predict: What does it print?
    22.1. reply["daily"] is the inner dictionary, ["temperature_2m_max"] is its list of temperatures, and [1] is the second one.
  2. The question you send: What does params=canberra do?
    Adds ?latitude=-35.28&longitude=149.13&... to the address, the query string the API reads
    requests builds the query string from the dictionary. It is the other side of Flask's request.args: here your program is the one asking.
  3. When it fails: Why does the program keep a saved weather.json?
    So it still works when the network, the school filter or the API fails
    Programs that use the internet must expect it to fail. Catching requests.RequestException and using saved data keeps the program useful.
  4. Spot the bug: This should print each day's top temperature, but it stops with KeyError: 'temperature_2m_max'. Click the line that is wrong.
    line 3
    The temperatures are inside reply["daily"], not reply itself. Each level needs its own key: reply["daily"]["temperature_2m_max"][i].
  5. Where this leads: Open-Meteo is free for non-commercial use if you credit it. Why does the program print a credit line?
    Using someone's data comes with terms: here, saying where it came from
    Data has owners and licences. Open-Meteo's data is CC BY 4.0: free to use with credit. Always check an API's terms before you build on it.

Solutions

One possible solution for each task. Other correct programs are fine.

Modify › The hottest day

import json
import requests

URL = "https://api.open-meteo.com/v1/forecast"
canberra = {"latitude": -35.28, "longitude": 149.13, "timezone": "Australia/Sydney", "daily": "temperature_2m_max,temperature_2m_min"}

try:
    reply = requests.get(URL, params=canberra, timeout=10).json()
    print("Live forecast from Open-Meteo")
except requests.RequestException:
    with open("weather.json") as f:
        reply = json.load(f)
    print("No connection: using the saved forecast")

days = reply["daily"]
for i in range(len(days["time"])):
    print(days["time"][i] + ": " + str(days["temperature_2m_min"][i]) + " to " + str(days["temperature_2m_max"][i]) + " degrees")

hottest = 0
for i in range(len(days["time"])):
    if days["temperature_2m_max"][i] > days["temperature_2m_max"][hottest]:
        hottest = i
print("Hottest: " + days["time"][hottest] + " at " + str(days["temperature_2m_max"][hottest]) + " degrees")
print("Weather data by Open-Meteo.com (CC BY 4.0)")

Modify › Rain

import json
import requests

URL = "https://api.open-meteo.com/v1/forecast"
canberra = {"latitude": -35.28, "longitude": 149.13, "timezone": "Australia/Sydney", "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum"}

try:
    reply = requests.get(URL, params=canberra, timeout=10).json()
    print("Live forecast from Open-Meteo")
except requests.RequestException:
    with open("weather.json") as f:
        reply = json.load(f)
    print("No connection: using the saved forecast")

days = reply["daily"]
for i in range(len(days["time"])):
    line = days["time"][i] + ": " + str(days["temperature_2m_min"][i]) + " to " + str(days["temperature_2m_max"][i]) + " degrees"
    if days["precipitation_sum"][i] > 0:
        line = line + ", rain " + str(days["precipitation_sum"][i]) + " mm"
    print(line)
print("Weather data by Open-Meteo.com (CC BY 4.0)")

two places

import json
import requests

URL = "https://api.open-meteo.com/v1/forecast"
places = {
    "Canberra": {"latitude": -35.28, "longitude": 149.13},
    "Darwin": {"latitude": -12.46, "longitude": 130.84},
}

averages = {}
for name in places:
    query = {"latitude": places[name]["latitude"], "longitude": places[name]["longitude"], "daily": "temperature_2m_max", "timezone": "auto"}
    try:
        days = requests.get(URL, params=query, timeout=10).json()["daily"]
        total = 0
        for t in days["temperature_2m_max"]:
            total = total + t
        averages[name] = total / len(days["temperature_2m_max"])
        print(name + ": " + str(round(averages[name], 1)) + " degrees on average")
    except requests.RequestException:
        print("Could not reach Open-Meteo for " + name)

if len(averages) == 2:
    difference = averages["Darwin"] - averages["Canberra"]
    print("Darwin is " + str(round(difference, 1)) + " degrees warmer on average")
print("Weather data by Open-Meteo.com (CC BY 4.0)")

Stretch and support

Stretch: Put the two-place comparison behind a Gradio app with a dropdown of places, or ask Open-Meteo for hourly temperatures and chart today with text bars.

Support: Give students the hottest-day loop with the comparison line blank, and ask them to finish it.

Exit ticket

Why does a program that uses an API need a plan for when the API cannot be reached?

Networks, filters and services fail; catching the error and using saved data (or saying so) keeps the program working instead of crashing.

What each step is for

Where it leads

Machine learning with Gradio uses data in the same way: tables in, answers out. Web apps with Flask builds the other side: an API of your own.