Tutorial

python-esios: Download Spanish electricity data in a few lines of Python

A Python library that wraps the ESIOS API into simple method calls — search indicators, fetch historical data, and get DataFrames with automatic caching.

python-esios: Download Spanish electricity data in a few lines of Python

Spain’s electricity system runs on 2,000+ indicators published by REE through the ESIOS API — prices, demand, generation by technology, cross-border flows, and more. But working directly with the API means writing boilerplate HTTP requests, handling pagination for long date ranges, and converting nested JSON into usable DataFrames.

python-esios wraps all of that into a clean Python interface. In this article, we’ll walk through the library’s main features and build real analysis with 4 practical use cases.

Before vs after: the “aha” moment

Without python-esios, fetching a week of Spanish electricity prices requires:

# Raw requests approach: manual headers, pagination, JSON parsing
import requests
import pandas as pd

TOKEN = "your-token-here"
headers = {
    "Accept": "application/json; application/vnd.esios-api-v1+json",
    "Content-Type": "application/json",
    "Host": "api.esios.ree.es",
    "x-api-key": TOKEN,
}

# You also need to handle chunking for long date ranges,
# timezone conversion, and JSON-to-DataFrame conversion manually
response = requests.get(
    "https://api.esios.ree.es/indicators/600",
    headers=headers,
    params={"start_date": "2024-06-01", "end_date": "2024-06-07T23:59:59"},
)
data = response.json()["indicator"]["values"]
df = pd.DataFrame(data)
# Still need to: parse datetimes, filter by country, set index...

With python-esios:

# python-esios: 3 lines
from esios import ESIOSClient
client = ESIOSClient()
df = client.indicators.get(600).historical("2024-06-01", "2024-06-07")

Key differences:

  • No boilerplate — token from env, headers handled internally
  • Automatic chunking — long date ranges are split into API-friendly windows
  • Parquet caching — repeated queries are served from local cache instantly
  • Clean DataFrames — DatetimeIndex with Europe/Madrid timezone, geo-pivoted columns

Installation

pip install python-esios

You’ll need an ESIOS API token. Request one for free from REE’s API page. Set it as an environment variable:

export ESIOS_API_KEY=your-token-here

Exploring the ESIOS catalog

The ESIOS API exposes 2,000+ indicators. Let’s see how to navigate them.

List all indicators

df_indicators = client.indicators.list()
df_indicators[["name", "short_name"]].head(10)
Table with 10 rows

Search by name

client.indicators.search("eólica")
Table with 60 rows

Get an indicator’s metadata

handle = client.indicators.get(600)
handle
Out
<IndicatorHandle id=600 name='Precio mercado SPOT Diario'>

Each handle exposes the indicator’s available geographies:

handle.geos
Out6 lines
[{'geo_id': 1, 'geo_name': 'Portugal'},
 {'geo_id': 2, 'geo_name': 'Francia'},
 {'geo_id': 3, 'geo_name': 'España'},
 {'geo_id': 8826, 'geo_name': 'Alemania'},
 {'geo_id': 8827, 'geo_name': 'Bélgica'},
 {'geo_id': 8828, 'geo_name': 'Países Bajos'}]

You can resolve geography names to IDs:

handle.resolve_geo("España")
Out
3

Use case 1: Day-ahead electricity prices

Indicator 600 is the SPOT market price. Let’s compare Spain, France, and Portugal for a week in June 2024:

price = client.indicators.get(600)
df_price = price.historical("2024-06-01", "2024-06-07")
df_price = df_price[["España", "Francia", "Portugal"]]
df_price.head()
Table with 5 rows
fig = px.line(
    df_price,
    title="Day-ahead electricity prices (June 2024)",
    labels={"value": "€/MWh", "datetime": ""},
)
fig.update_traces(line_shape="hv")
fig.update_layout(legend_title_text="Country")
fig.show()

Price statistics

df_price.describe().round(2)
Table with 8 rows

Use case 2: Real-time demand

Indicator 1293 tracks actual electricity demand at 5-minute resolution:

demand = client.indicators.get(1293)
df_demand = demand.historical("2024-06-01", "2024-06-03")
df_demand.head()
Table with 5 rows
fig = px.line(
    df_demand,
    title="Real-time electricity demand — Spain (June 2024)",
    labels={"value": "MW", "datetime": ""},
)
fig.update_layout(showlegend=False)
fig.show()

Use case 3: Generation mix — wind vs solar

Let’s compare wind and solar photovoltaic P48 scheduled generation (the latest program before real-time):

# 82 = Wind onshore P48, 84 = Solar PV P48
df_gen = client.indicators.compare(
    [82, 84],
    "2024-06-01", "2024-06-07",
)
df_gen.head()
Table with 5 rows
fig = px.area(
    df_gen,
    title="Wind vs Solar PV scheduled generation P48 — Spain (June 2024)",
    labels={"value": "MW", "datetime": ""},
)
fig.update_layout(legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0), legend_title_text="", title_y=1.0, title_yanchor="top", margin=dict(t=80))
fig.show()

Daily totals

daily = df_gen.resample("D").sum() / 1000  # Convert MW·5min to GWh approx
daily.columns = ["Wind (GWh)", "Solar PV (GWh)"]
daily.round(1)
Table with 7 rows

Use case 4: Cross-country price comparison

The compare() method makes it easy to fetch multiple indicators into one DataFrame. Let’s look at how Spain’s price compares to demand patterns:

df_compare = client.indicators.compare(
    [600, 1293],
    "2024-06-01", "2024-06-03",
)
df_compare = df_compare.ffill()  # Forward-fill hourly price into 5-min rows
df_compare.head()
Table with 5 rows
fig = go.Figure()

fig.add_trace(go.Scatter(
    x=df_compare.index,
    y=df_compare.iloc[:, 0],
    name="Price (€/MWh)",
    yaxis="y",
    line_shape="hv",
))

fig.add_trace(go.Scatter(
    x=df_compare.index,
    y=df_compare.iloc[:, 1],
    name="Demand (MW)",
    yaxis="y2",
    opacity=0.6,
))

fig.update_layout(
    title="Price vs Demand — Spain (June 2024)",
    yaxis=dict(title="€/MWh"),
    yaxis2=dict(title="MW", overlaying="y", side="right"),
    legend=dict(x=0.01, y=0.99),
)
fig.show()

Conclusions

python-esios turns the ESIOS API into a Pythonic interface:

  • client.indicators.list() — browse the full catalog
  • client.indicators.search("...") — find indicators by name
  • client.indicators.get(id).historical(start, end) — fetch data as DataFrames
  • client.indicators.compare([...], start, end) — merge multiple indicators
  • Automatic caching — parquet files for instant re-queries
  • Geo resolutionresolve_geo("España") instead of memorizing IDs

For more details, check the GitHub repository.

Resources

Keep reading

Related articles you might enjoy

Table of Contents
Search sections

Subscribe to our newsletter

Get weekly insights on data, automation, and AI.

© 2026 Datons. All rights reserved.