Vai al contenuto

Il meteo da un'API

In questa lezione impari a

  • capire cos'Γ¨ un'API esterna e che cosa restituisce
  • fare una richiesta con requests e leggere il JSON
  • prendere dal JSON solo i dati che servono
  • mostrare i dati in una pagina Flask
  • gestire il caso "l'API non risponde"

Fino a oggi i dati delle vostre applicazioni li avete scritti voi: una lista in app.py, un database SQLite. Ma moltissime app che usate ogni giorno (meteo, mappe, orari dei treni) i dati li chiedono a un altro computer, attraverso un'API.

Oggi facciamo proprio questo: chiediamo il meteo a un servizio gratuito, Open-Meteo, e lo mostriamo in una piccola dashboard.

Il kit della lezione

Si avvia come i kit del corso Flask (se serve, rileggete Come si lavora con i kit):

pip install -r requirements.txt
python meteo.py
python app.py
File A cosa serve
meteo.py passo 1: una sola richiesta all'API, il risultato si legge nel terminale
app.py passo 2: la dashboard Flask
templates/ le pagine HTML
dati_esempio.json una risposta dell'API salvata: si usa quando Internet non c'Γ¨
requirements.txt le librerie: flask e requests

Che cos'Γ¨ un'API, in breve

Un'API Γ¨ un indirizzo web che invece di una pagina restituisce dati. Il nostro programma fa una domanda, il server risponde in JSON.

   il nostro programma                          Open-Meteo
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   1. domanda (GET)    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚                   β”‚ ────────────────────► β”‚              β”‚
  β”‚  "che tempo fa a  β”‚                       β”‚   server     β”‚
  β”‚   Torino?"        β”‚ ◄──────────────────── β”‚   meteo      β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   2. risposta (JSON)  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

È esattamente quello che faceva il client.py della bacheca nel corso Flask, solo che stavolta il server non è nostro.

Passo 0: guardare l'API nel browser

Prima di scrivere codice, guardiamo cosa risponde l'API. Aprite questo indirizzo nel browser:

api.open-meteo.com/v1/forecast?latitude=45.07&longitude=7.69¤t=temperature_2m,wind_speed_10m&timezone=Europe/Rome

L'indirizzo ha due parti:

https://api.open-meteo.com/v1/forecast  ?  latitude=45.07 & longitude=7.69 & current=temperature_2m,...
└──────────── l'API β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     └──────────── i parametri: DOVE e COSA β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

E la risposta è più o meno così (l'abbiamo ordinata per leggerla meglio):

{
  "latitude": 45.06,
  "longitude": 7.68,
  "current": {
    "time": "2026-10-24T11:00",
    "temperature_2m": 14.2,
    "wind_speed_10m": 6.1
  }
}

temperature_2m vuol dire "temperatura a 2 metri dal suolo": Γ¨ quella dei bollettini. Firefox mostra il JSON giΓ  colorato e ordinato; in Chrome provate la casella Formatta.

Passo 1: la stessa richiesta in Python (meteo.py)

Il browser fa la richiesta per noi. In Python la fa la libreria requests:

import requests                     # la libreria per fare richieste HTTP

URL = "https://api.open-meteo.com/v1/forecast"

parametri = {
    "latitude": 45.07,                                  # latitudine di Torino
    "longitude": 7.69,                                  # longitudine di Torino
    "current": "temperature_2m,wind_speed_10m",         # i dati di adesso
    "timezone": "Europe/Rome",                          # orari italiani
}

risposta = requests.get(URL, params=parametri, timeout=10)   # 1. la richiesta
print("Codice di stato:", risposta.status_code)              # 2. 200 = tutto ok
dati = risposta.json()                                        # 3. JSON -> dizionario
adesso = dati["current"]                                      # 4. la parte che ci serve
print("Temperatura:", adesso["temperature_2m"], "Β°C")

Riga per riga:

  1. requests.get(...) fa la richiesta. Con params= passiamo i parametri come dizionario: Γ¨ requests che li attacca all'indirizzo dopo il ?. timeout=10 vuol dire "se dopo 10 secondi non risponde, lascia perdere".
  2. status_code Γ¨ il codice di stato HTTP: 200 tutto bene, 400 abbiamo sbagliato un parametro.
  3. .json() trasforma il testo JSON in un dizionario Python.
  4. Da lì in poi è Python normale: parentesi quadre e chiavi.

Il trucco per non perdersi nel JSON Γ¨ leggerlo come una scatola dentro una scatola:

dati                          ← tutto il dizionario
 └─ dati["current"]           ← la scatola "current"
     └─ dati["current"]["temperature_2m"]   β†’  14.2

Avviate python meteo.py: vedete la temperatura di Torino di questo momento.

Passo 2: la dashboard (app.py)

Ora mettiamo tutto in una pagina web. Il nostro server Flask, quando qualcuno apre la pagina, fa lui la richiesta a Open-Meteo:

  browser            nostro server Flask              Open-Meteo
     β”‚  GET /citta/Roma      β”‚                              β”‚
     β”‚ ────────────────────► β”‚  requests.get(...)           β”‚
     β”‚                       β”‚ ───────────────────────────► β”‚
     β”‚                       β”‚ ◄─────────────────────────── β”‚
     β”‚                       β”‚  JSON -> dizionario          β”‚
     β”‚  pagina HTML          β”‚  render_template(...)        β”‚
     β”‚ ◄──────────────────── β”‚                              β”‚

Il browser non parla mai con Open-Meteo: parla solo con il nostro server, che fa da "intermediario".

Le cittΓ 

CITTA = {
    "Torino": (45.07, 7.69),
    "Milano": (45.46, 9.19),
    "Roma": (41.89, 12.48),
    "Palermo": (38.12, 13.36),
}

Un dizionario: il nome della cittΓ  e le sue coordinate (latitudine, longitudine). Nella pagina, i pulsanti in alto sono creati con un ciclo for su questo dizionario: aggiungendo una riga qui, compare un pulsante in piΓΉ.

Il tempo diventa una parola

L'API non scrive "pioggia": manda un numero, il codice meteo (weather_code). Ad esempio 0 Γ¨ sereno, 61 pioggia debole, 95 temporale. Un dizionario lo traduce:

DESCRIZIONI = {
    0: "β˜€οΈ Sereno", 1: "🌀️ Poco nuvoloso", 2: "β›… Nuvoloso", 3: "☁️ Coperto",
    61: "🌧️ Pioggia debole", 63: "🌧️ Pioggia", 95: "β›ˆοΈ Temporale",
    # ... e altri
}

def descrivi(codice):
    return DESCRIZIONI.get(codice, f"Codice {codice}")

.get(codice, ...) restituisce la descrizione, oppure "Codice 77" se quel numero non è nel dizionario: così il programma non si blocca mai.

Chiedere il meteo (e cavarsela senza Internet)

def chiedi_meteo(lat, lon):
    parametri = {
        "latitude": lat,
        "longitude": lon,
        "current": "temperature_2m,wind_speed_10m,weather_code",            # adesso
        "daily": "weather_code,temperature_2m_max,temperature_2m_min",      # giorno per giorno
        "forecast_days": 3,                                                 # quanti giorni
        "timezone": "Europe/Rome",
    }
    try:
        risposta = requests.get(URL, params=parametri, timeout=5)
        risposta.raise_for_status()          # se il codice non Γ¨ 200 -> va all'except
        return risposta.json(), False        # dati veri
    except requests.RequestException:
        file = Path(__file__).with_name("dati_esempio.json")
        return json.loads(file.read_text(encoding="utf-8")), True
  • Stavolta chiediamo due cose: current (adesso) e daily (i prossimi giorni).
  • Un'API esterna puΓ² non rispondere: manca la rete, il server Γ¨ spento, il proxy della scuola blocca. Senza try, la pagina mostrerebbe un errore. Con try/except, se qualcosa va storto leggiamo la risposta salvata nel file dati_esempio.json.
  • La funzione restituisce due valori: i dati e True/False per dire se sono quelli di esempio. La pagina, in quel caso, mostra un avviso rosso.

Provate: staccate il cavo di rete (o disattivate il Wi-Fi) e ricaricate la pagina. Funziona ancora, ma compare l'avviso "dati di esempio".

I giorni: da colonne a righe

La parte daily della risposta è fatta così, a colonne: una lista per ogni dato.

"daily": {
  "time":               ["2026-10-24", "2026-10-25", "2026-10-26"],
  "weather_code":       [2,            61,           3],
  "temperature_2m_max": [16.8,         13.1,         14.5],
  "temperature_2m_min": [8.3,          9.0,          7.6]
}

I dati dello stesso giorno stanno nella stessa posizione di ogni lista. Il giorno 0 Γ¨ time[0], weather_code[0], temperature_2m_max[0]... Per il template Γ¨ piΓΉ comodo averli a righe, un dizionario per giorno:

def prepara_giorni(daily):
    giorni = []
    for i in range(len(daily["time"])):                 # i = 0, 1, 2
        data = daily["time"][i]                         # "2026-10-24"
        giorni.append({
            "data": data[8:10] + "/" + data[5:7],       # diventa "24/10"
            "tempo": descrivi(daily["weather_code"][i]),
            "massima": daily["temperature_2m_max"][i],
            "minima": daily["temperature_2m_min"][i],
        })
    return giorni
  A COLONNE (come arriva)                A RIGHE (come piace al template)
  time    [24/10, 25/10, 26/10]          [ {data: 24/10, massima: 16.8, ...},
  max     [16.8,  13.1,  14.5 ]   ──►      {data: 25/10, massima: 13.1, ...},
  min     [8.3,   9.0,   7.6  ]            {data: 26/10, massima: 14.5, ...} ]

data[8:10] prende i caratteri dalla posizione 8 alla 9 della stringa (il giorno), data[5:7] il mese.

La route

@app.route("/citta/<nome>")
def citta(nome):
    if nome not in CITTA:
        abort(404)                                  # cittΓ  che non conosciamo
    lat, lon = CITTA[nome]                          # le sue coordinate
    dati, esempio = chiedi_meteo(lat, lon)          # chiediamo all'API

    adesso = dati["current"]
    return render_template(
        "index.html",
        nome=nome,
        elenco_citta=CITTA.keys(),
        temperatura=adesso["temperature_2m"],
        vento=adesso["wind_speed_10m"],
        tempo=descrivi(adesso["weather_code"]),
        ora=adesso["time"][11:16],                  # "2026-10-24T11:00" -> "11:00"
        giorni=prepara_giorni(dati["daily"]),
        esempio=esempio,
    )

È lo schema che conoscete dal corso Flask: prendo i dati, li preparo, li passo al template. L'unica differenza è da dove arrivano. La home / mostra semplicemente Torino.

Il template

In templates/index.html niente di nuovo: variabili tra {{ }}, un if per l'avviso e due for, uno per i pulsanti delle cittΓ  e uno per le schede dei giorni:

{% for g in giorni %}
  <div class="scheda">
    <h3>{{ g.data }}</h3>
    <p>{{ g.tempo }}</p>
    <p>πŸ”Ί {{ g.massima }} Β°C &nbsp; πŸ”» {{ g.minima }} Β°C</p>
  </div>
{% endfor %}

Se qualcosa non va

Problema Causa Soluzione
ModuleNotFoundError: No module named 'requests' libreria non installata pip install -r requirements.txt (oppure py -m pip install --user requests)
La pagina mostra sempre "dati di esempio" il PC non raggiunge Open-Meteo (rete, proxy) provate l'indirizzo del Passo 0 nel browser; se non si apre, il kit va bene lo stesso: lavorate con i dati di esempio
meteo.py si ferma con ConnectionError come sopra: niente rete meteo.py non ha il try, apposta: leggete l'errore, Γ¨ quello che app.py intercetta
KeyError: 'current' nei parametri manca current, o c'Γ¨ un errore di battitura stampate dati con print(dati) e guardate cosa Γ¨ arrivato davvero
Codice di stato 400 un parametro sbagliato (es. temperatura_2m) la risposta spiega il motivo: print(risposta.text)

Il trucco dei professionisti: quando non sapete cosa contiene una risposta, stampatela. print(dati) nel terminale, oppure aprite l'indirizzo nel browser.

Missioni

⭐ Missione 1 – La mia cittΓ 

Aggiungete al dizionario CITTA il vostro paese o la vostra cittΓ . Le coordinate si trovano su Google Maps: tasto destro sul punto β†’ il primo numero Γ¨ la latitudine, il secondo la longitudine. Ricaricate: compare un pulsante nuovo.

⭐ Missione 2 – Cinque giorni

Fate in modo che la dashboard mostri le previsioni di 5 giorni invece di 3. Basta cambiare un numero in chiedi_meteo. (Serve la connessione: i dati di esempio hanno solo 3 giorni.)

⭐⭐ Missione 3 – L'umiditΓ 

Mostrate anche l'umiditΓ  di adesso, sotto il vento: "πŸ’§ UmiditΓ  70 %". Il dato si chiama relative_humidity_2m. Servono tre modifiche: aggiungerlo a current nei parametri, passarlo al template nella route, scriverlo in index.html. Provatelo prima nel browser, aggiungendolo all'indirizzo del Passo 0.

Aiutino

Nei parametri: "current": "temperature_2m,wind_speed_10m,weather_code,relative_humidity_2m". Nella route: umidita=adesso.get("relative_humidity_2m", "?") (con .get la pagina funziona anche con i dati di esempio, che l'umiditΓ  non ce l'hanno).

⭐⭐ Missione 4 – Allerta caldo e gelo

Nelle schede dei giorni aggiungete un'etichetta colorata: Β«Caldo!Β» (con le classi etichetta rossa) se la massima Γ¨ almeno 30 Β°C, Β«GeloΒ» (con la classe etichetta) se la minima Γ¨ 0 Β°C o meno. Si fa solo nel template, con un if. Per provarla senza aspettare l'estate, cambiate i numeri in dati_esempio.json e staccate la rete.

⭐⭐⭐ Missione 5 – Che aria tira

Open-Meteo ha anche un'API per la qualitΓ  dell'aria, sempre senza chiave: https://air-quality-api.open-meteo.com/v1/air-quality con i parametri latitude, longitude e current=pm10,pm2_5. Scrivete una funzione chiedi_aria(lat, lon) sul modello di chiedi_meteo e mostrate nella pagina le polveri sottili PM10 (in Β΅g/mΒ³). Se l'API non risponde, scrivete "non disponibile".

Aiutino

Provate prima l'indirizzo nel browser per vedere com'Γ¨ fatta la risposta: il valore Γ¨ in dati["current"]["pm10"]. Nel except la funzione puΓ² restituire semplicemente None, e nel template: {% if pm10 is none %}non disponibile{% else %}{{ pm10 }} Β΅g/mΒ³{% endif %}. Il limite giornaliero di legge per il PM10 Γ¨ 50 Β΅g/mΒ³: se volete, mettete un'etichetta rossa quando lo supera.

⭐⭐⭐ Missione 6 – Cerca una cittΓ 

Aggiungete in alto un piccolo form con una casella di testo: si scrive il nome di una cittΓ  qualsiasi (es. "Bra") e si vede il suo meteo. Le coordinate le dΓ  un'altra API di Open-Meteo, il geocoding: https://geocoding-api.open-meteo.com/v1/search?name=Bra&count=1&language=it.

Aiutino

Form: <form action="{{ url_for('cerca') }}"><input name="q"> <button>Cerca</button></form> (senza method Γ¨ una GET). Route /cerca: q = request.args.get("q", ""), poi requests.get(...) con params={"name": q, "count": 1, "language": "it"}. Nella risposta, dati.get("results") Γ¨ una lista: se Γ¨ vuota o manca, la cittΓ  non esiste; altrimenti r = dati["results"][0] e le coordinate sono r["latitude"] e r["longitude"]. Per non riscrivere tutto, spostate il corpo della route citta in una funzione mostra(nome, lat, lon) e chiamatela da entrambe le route.