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:
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_2mvuol 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:
requests.get(...)fa la richiesta. Conparams=passiamo i parametri come dizionario: è requests che li attacca all'indirizzo dopo il?.timeout=10vuol dire "se dopo 10 secondi non risponde, lascia perdere".status_codeè il codice di stato HTTP: 200 tutto bene, 400 abbiamo sbagliato un parametro..json()trasforma il testo JSON in un dizionario Python.- 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) edaily(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. Contry/except, se qualcosa va storto leggiamo la risposta salvata nel filedati_esempio.json. - La funzione restituisce due valori: i dati e
True/Falseper 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 π» {{ 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.