CLIMATE / Analysis

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.

climaRevolt
climaRevolt ExpertsWritten and checked by our specialists · 27 October 2026
9 min read
How to Download and Analyse ERA5 in Python with xarray (New CDS, 2026)

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:

~/.cdsapirc · text
url: https://cds.climate.copernicus.eu/api
key: YOUR-PERSONAL-ACCESS-TOKEN

Old guides show a key in the form “UID:API-key”. That format no longer works.

Step 2: download monthly 2 m temperature

Request ERA5 monthly means for one area · Python
# 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

Open the file and normalise the time dimension · Python
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 Celsius

Always 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

Area-weighted mean, annual means and anomalies against 1991–2020 · Python
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.

THE CLIMAREVOLT WEEKLY

Get the climate stories that matter, explained, every Sunday.

One email a week: new explainers, data and free tools. No spam, unsubscribe any time.

ShareXLi