How to Download and Analyse ERA5 in Python with xarray (New CDS, 2026)
Download ERA5 with the new CDS API, open it in xarray (valid_time, zipped files), and chart annual temperature anomalies for any area.

In brief
- ERA5 is the ECMWF climate reanalysis: hourly weather for the whole world from 1940 to the present, free from the Copernicus Climate Data Store (CDS).
- Since the CDS moved to its new system in 2024, your API key is a single personal access token, and requests use data_format and download_format.
- NetCDF files from the new CDS use a valid_time dimension instead of time, and some requests arrive as a zip with separate files. Code written for the old system breaks on both.
- For trends, start with the monthly means dataset: the files are small and the analysis is quick.
ERA5 is the dataset most climate analysts reach for first: consistent, global and free. Getting it into Python used to be well documented, but the Climate Data Store changed in 2024, and many tutorials still show the old key format and the old file layout. This walkthrough uses the current system, from setting up the key to a chart of annual temperature anomalies for any area.
Step 1: account, licence and key
Register at the Climate Data Store. Open the ERA5 dataset page and accept its licence once, otherwise every request fails. Then copy your personal access token from your profile and save it in a file called .cdsapirc in your home folder:
url: https://cds.climate.copernicus.eu/api
key: YOUR-PERSONAL-ACCESS-TOKENOld guides show a key in the form “UID:API-key”. That format no longer works.
Step 2: download monthly 2 m temperature
# pip install "cdsapi>=0.7.2" xarray netCDF4 matplotlib
import cdsapi
client = cdsapi.Client()
client.retrieve(
"reanalysis-era5-single-levels-monthly-means",
{
"product_type": ["monthly_averaged_reanalysis"],
"variable": ["2m_temperature"],
"year": [str(y) for y in range(1991, 2026)],
"month": [f"{m:02d}" for m in range(1, 13)],
"time": ["00:00"],
"area": [52, -1, 51, 1], # north, west, south, east (here: around London)
"data_format": "netcdf",
"download_format": "unarchived",
},
"era5_t2m_monthly.nc",
)Requests join a queue, so a small one like this can still take a few minutes. Change the area to your own; keep it small while you learn.
Step 3: open it, and handle the new layout
import zipfile
import numpy as np
import xarray as xr
path = "era5_t2m_monthly.nc"
if zipfile.is_zipfile(path): # some requests still arrive zipped
with zipfile.ZipFile(path) as z:
z.extractall("era5_files")
path = "era5_files/" + z.namelist()[0]
ds = xr.open_dataset(path)
print(ds) # check the dimension names first
if "valid_time" in ds.dims: # new CDS files
ds = ds.rename({"valid_time": "time"})
t2m = ds["t2m"] - 273.15 # kelvin to degrees CelsiusAlways print the dataset once. If you request several variables, accumulated ones such as precipitation and instantaneous ones such as temperature can come back as separate files inside the zip; open each one.
Step 4: area average and anomalies
weights = np.cos(np.deg2rad(t2m.latitude))
series = t2m.weighted(weights).mean(["latitude", "longitude"])
annual = series.resample(time="YS").mean()
baseline = annual.sel(time=slice("1991", "2020")).mean()
anomaly = annual - baseline
ax = anomaly.plot.line(marker="o")
ax[0].axes.axhline(0, color="grey", linewidth=0.8)
ax[0].axes.set_ylabel("°C vs 1991–2020")
ax[0].axes.set_title("Annual mean 2 m temperature anomaly")The cosine weights matter for larger areas, because grid cells shrink towards the poles. The 1991–2020 baseline matches the current World Meteorological Organization climate normal, so your numbers are comparable with published anomalies. Leave out the current year if it is not yet complete.
Where to go next
The same pattern works for rainfall (total_precipitation, in metres), wind and soil moisture. For hourly data, use reanalysis-era5-single-levels and request only the hours and days you need; for land-only detail at about 9 km, try ERA5-Land. For climate projections rather than the past, the CDS also hosts CMIP6 data, which xarray opens the same way.
Frequently asked questions
Why does my ERA5 file have valid_time instead of time?
NetCDF files from the new Climate Data Store (2024 onwards) name the time dimension valid_time. Rename it with ds.rename({"valid_time": "time"}).
Why did my ERA5 download arrive as a zip?
Some requests are split into several files, for example accumulated and instantaneous variables, and delivered as a zip even with download_format set to unarchived. Check with zipfile.is_zipfile and extract it.
How do I set up the CDS API key?
Put url: https://cds.climate.copernicus.eu/api and key: followed by your personal access token in a .cdsapirc file in your home folder, and accept the dataset licence on its web page.
What is the difference between ERA5 and ERA5-Land?
ERA5 covers land, ocean and atmosphere at about 31 km. ERA5-Land re-runs the land component at about 9 km for more detail over land.
Get the climate stories that matter, explained, every Sunday.
One email a week: new explainers, data and free tools. No spam, unsubscribe any time.
More from climaRevolt

El Niño 2026: What a Record-Strength El Niño Means for Winter and the World

What Is a Heat Dome? The Weather Pattern Behind 2026’s Deadly Summer
