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.

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

Subscribe to our newsletter

Get weekly insights on data, automation, and AI.

© 2026 Datons. All rights reserved.