How to Get Weather Data for Many Locations in Python

On this page

Problem statement

Weather for one place is one request; weather for a hundred stores, farms or cities needs a plan. Looping over coordinates one call at a time is slow and soon hits the API's rate limit. Requesting all of them at once needs the right syntax and returns nested results that must be matched back to the input rows. The data also needs to come back in a shape that joins to the original table: one row per place and date, with the place's identifier.

This page gets the daily maximum temperature for July 2024 for all 50 US state capitals from the Open-Meteo archive in batched requests. It reshapes the result into a tidy table, ranks the capitals, and maps the month's heat.

Quick answer

Pass comma-separated latitudes and longitudes in one request; the response is a list in the same order:

def batch(rows, **params):
    p = {"latitude": ",".join(f"{v:.4f}" for v in rows.lat), "longitude": ",".join(f"{v:.4f}" for v in rows.lon), **params}
    js = cached_get("https://archive-api.open-meteo.com/v1/archive", p)
    js = js if isinstance(js, list) else [js]
    return pd.concat([pd.DataFrame(j["daily"]).assign(capital=name, cell_lat=j["latitude"], cell_lon=j["longitude"]) for j, name in zip(js, rows.name)])
W = pd.concat([batch(CAP.iloc[i:i + 10], start_date="2024-07-01", end_date="2024-07-31", daily="temperature_2m_max", timezone="auto")
               for i in range(0, len(CAP), 10)])
W["time"] = pd.to_datetime(W.time)
print(f"{W.capital.nunique()} capitals x {W.time.nunique()} days = {len(W):,} rows; {len(range(0, len(CAP), 10))} requests")
50 capitals x 31 days = 1,550 rows; 5 requests

Five requests of ten capitals each return 1,550 rows, one for each capital and day of July 2024.

Map of the 48 contiguous state capitals coloured by July 2024 mean daily maximum and sized by days at 35 °C or more, with Phoenix largest and darkest, Sacramento, Boise and Salt Lake City hot, and northern capitals cooler. Map of the 48 contiguous state capitals coloured by July 2024 mean daily maximum and sized by days at 35 °C or more, with Phoenix largest and darkest, Sacramento, Boise and Salt Lake City hot, and northern capitals cooler.
July 2024 in 48 capitals.

Summaries per place, wide or long, joined back

1. Summarise per place

S = W.groupby("capital").temperature_2m_max.agg(["mean", "max"]).join(CAP.set_index("name")[["state"]])
S["days_35"] = W.assign(hot=W.temperature_2m_max >= 35).groupby("capital").hot.sum()
print("hottest July means:", S.nlargest(4, "mean")[["state", "mean"]].round(1).to_dict("index"))
print("coolest July means:", S.nsmallest(3, "mean")[["state", "mean"]].round(1).to_dict("index"))
hottest July means: {'Phoenix': {'state': 'Arizona', 'mean': 42.8}, 'Sacramento': {'state': 'California', 'mean': 39.4}, 'Boise': {'state': 'Idaho', 'mean': 36.0}, 'Salt Lake City': {'state': 'Utah', 'mean': 35.7}}
coolest July means: {'Juneau': {'state': 'Alaska', 'mean': 11.8}, 'Madison': {'state': 'Wisconsin', 'mean': 26.3}, 'St.  Paul': {'state': 'Minnesota', 'mean': 27.4}}

Phoenix had the hottest July, with a mean daily maximum of 42.8 °C, followed by Sacramento, Boise and Salt Lake City. Juneau was by far the coolest at 11.8 °C, then Madison and St. Paul.

State capitals with the hottest July 2024 mean daily maximum. hottest capitals, July 2024Phoenix42.8 °CSacramento39.4 °CBoise36.0 °CSalt Lake City35.7 °CJuneau (coolest)11.8 °C015 °C30 °C45 °C50 capitals in 5 requests, Open-Meteo archive spatialworkflow.io
Hottest and coolest.

2. Wide or long

A long table (one row per place and day) joins and groups easily; a wide table (one column per place) suits time-series plots:

wide = W.pivot(index="time", columns="capital", values="temperature_2m_max")
print(f"long: {W.shape}; wide: {wide.shape}; days with a missing value in wide: {wide.isna().any(axis=1).sum()}")
long: (1550, 5); wide: (31, 50); days with a missing value in wide: 0

The long table has 1,550 rows and the wide one 31 rows by 50 columns, with no missing values.

3. Join back to the places

G = gpd.GeoDataFrame(CAP.merge(S.reset_index().rename(columns={"capital": "name"}), on=["name", "state"]),
                     geometry=gpd.points_from_xy(CAP.lon, CAP.lat), crs=4326)
print(f"capitals with 10 or more days at 35 °C or above: {(G.days_35 >= 10).sum()}: {', '.join(G[G.days_35 >= 10].sort_values('days_35', ascending=False).name)}")
capitals with 10 or more days at 35 °C or above: 8: Phoenix, Sacramento, Boise, Salt Lake City, Carson City, Oklahoma City, Austin, Denver

Eight capitals had 10 or more days at 35 °C or above, from Phoenix and Sacramento in the West to Oklahoma City and Austin.

The grid cell behind each capital

The data used on this page

The 50 US state capitals from Natural Earth populated places (1:10m), and daily maximum temperature from the Open-Meteo historical weather API (ERA5-based reanalysis). Responses are cached and requests retry after a 429.

import hashlib
import json
import time
from pathlib import Path

import geopandas as gpd
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
def cached_get(url, params):
    f = Path("data/om_" + hashlib.md5((url + json.dumps(params, sort_keys=True)).encode()).hexdigest() + ".json")
    if not f.exists():
        for attempt in range(6):
            r = requests.get(url, params=params, timeout=300)
            if r.status_code == 429:
                time.sleep(60 * (attempt + 1)); continue
            r.raise_for_status(); f.write_text(r.text); break
        time.sleep(5)
    return json.loads(f.read_text())

p = Path("data/ne_10m_populated_places.zip")
if not p.exists():
    r = requests.get("https://naciscdn.org/naturalearth/10m/cultural/ne_10m_populated_places.zip", timeout=600)
    r.raise_for_status(); p.write_bytes(r.content)
NE = gpd.read_file(p)
CAP = NE[(NE.ADM0NAME == "United States of America") & (NE.FEATURECLA == "Admin-1 capital")]
CAP = pd.DataFrame({"name": CAP.NAME, "state": CAP.ADM1NAME, "lat": CAP.LATITUDE, "lon": CAP.LONGITUDE}).sort_values("state").reset_index(drop=True)
print(f"{len(CAP)} state capitals")
50 state capitals

Natural Earth lists all 50 state capitals.

Example 1: the grid cell behind each capital

Each response reports the coordinates of the grid cell it used. Measure how far each capital is from its cell's centre, and whether any two share a cell:

cells = W.drop_duplicates("capital")[["capital", "cell_lat", "cell_lon"]].merge(CAP, left_on="capital", right_on="name")
d_km = gpd.GeoSeries(gpd.points_from_xy(cells.lon, cells.lat), crs=4326).to_crs(5070).distance(
       gpd.GeoSeries(gpd.points_from_xy(cells.cell_lon, cells.cell_lat), crs=4326).to_crs(5070)) / 1000
print(f"distance from capital to cell centre: median {d_km.median():.1f} km, largest {d_km.max():.1f} km ({cells.capital[d_km.idxmax()]})")
print(f"capitals sharing a cell with another: {cells.duplicated(['cell_lat', 'cell_lon'], keep=False).sum()}")
distance from capital to cell centre: median 3.3 km, largest 7.1 km (Juneau)
capitals sharing a cell with another: 0

The cell centres are a median 3.3 km from the capitals, and at most 7.1 km, for Juneau. No two capitals share a cell.

Long and wide shapes for weather at many locations. long or wideshapegood forlong1,550 x 5joins, group-bywide31 x 50time-series plotsGeoDataFrame50 placesmapsOne row per capital and day in the long table spatialworkflow.io
Long or wide.

Responses come back in request order

Batching many locations into Open-Meteo requests. many places, few requests50 capitalsNatural Earthbatches of 10comma-separatedJSON listsame orderzip with namesorder, not IDslong table1,550 rowsEach location still counts against the rate limit spatialworkflow.io
Many places, few requests.

Open-Meteo's APIs accept lists of coordinates, latitude=a,b,c with matching longitudes, and answer with a JSON list holding one object per location in the same order. The objects do not carry the caller's names or IDs, so matching relies on order, which is why the code zips the response with the input rows. Batching cuts the number of HTTP round trips and keeps the code simple. It does not cut the cost against the rate limit, because the free tier counts each location, and weighs long periods and many variables more heavily. Batches of around ten locations and a short pause between them keep a script well inside the limits. The same pattern works for the forecast and air-quality APIs.

The output shape decides how easily the weather joins to the rest of the work. A long table with a place identifier and a date is the natural form for joins to sales, crop yields or events, and for group-by summaries. A wide table suits plotting. A GeoDataFrame of places with their summary statistics is what a map needs. Each place's value comes from its nearest grid cell, so two places close together may share a cell and get identical weather.

Local days, many places and shared cells

Checks when getting weather for many locations. before batchingkeep input orderresponses carry no IDscache every batchre-runs cost nothingpause between batchesper-minute limittimezone=autolocal days per placecheck shared cellsnone among the capitalsMedian 3.3 km from capital to cell centre spatialworkflow.io
Before batching.
  • timezone=auto: each location's days are counted in its own local time zone.
  • Very many places: for thousands of points on a regular grid, gridded data (ERA5 as Zarr or NetCDF) is far more efficient than point requests.
  • Order, not names: never sort or deduplicate the inputs between building the request and reading the response.
  • Shared cells: places within one 0.25° cell get the same values; aggregate by cell first if that matters.

FAQ

How do I get weather data for multiple locations in Python?

Send comma-separated latitudes and longitudes to the Open-Meteo API and match the returned list to your locations by order.

Is it faster to request many locations at once?

It saves round trips, but each location still counts against the rate limit.

How do I join weather data to my locations?

Build a long table with a location ID and date, then merge it with your table on those columns.

Why do two nearby locations have identical weather?

They fall in the same grid cell of the underlying reanalysis.