Skip to main content

Python: Asynchronous client for the Open-Meteo API

GitHub Release Python Versions Project Stage Project Maintenance License

Build Status Code Coverage OpenSSF Scorecard Open in Dev Containers

Sponsor Frenck via GitHub Sponsors

Support Frenck on Patreon

Asynchronous Python client for the Open-Meteo API.

About

Open-Meteo offers free weather forecast APIs for open-source developers and non-commercial use. No API key is required.

This package is an asynchronous Python client for it, covering the weather forecast, air quality, geocoding, and elevation APIs. It is mainly created to allow third-party programs to use Open-Meteo data. Home Assistant, for example, uses it for its Open-Meteo integration.

Installation

pip install open-meteo

Usage

The client is an async context manager; every API call is a coroutine. A quick example that gets the current and hourly temperature in Enschede:

import asyncio

from open_meteo import HourlyParameters, OpenMeteo


async def main() -> None:
    """Show example of using the Open-Meteo API client."""
    async with OpenMeteo() as open_meteo:
        forecast = await open_meteo.forecast(
            latitude=52.27,
            longitude=6.87417,
            current=[HourlyParameters.TEMPERATURE_2M],
            hourly=[HourlyParameters.TEMPERATURE_2M],
        )
        print(f"It is {forecast.current.temperature_2m} °C in Enschede")


if __name__ == "__main__":
    asyncio.run(main())

Open-Meteo only returns the variables you ask for, so the variables and sections on the returned models are optional: those you did not request are None. A requested series can contain None values as well, where a weather model has no data, like at the end of a long forecast. Those keep their place, so the values stay aligned with their timestamps:

for time, temperature in zip(
    forecast.hourly.time, forecast.hourly.temperature_2m, strict=True
):
    if temperature is not None:
        print(time, temperature)

Timestamps are local time in the requested timezone, UTC by default, as naive datetimes. The offset to UTC is on the response, to make them aware when you need to compare them with other times:

from datetime import timedelta, timezone

offset = timezone(timedelta(seconds=forecast.utc_offset_seconds))
observed_at = forecast.current.time.replace(tzinfo=offset)

Weather forecast

Request current conditions, hourly data, and daily data in a single call. Every hourly variable is also available as a current condition.

from open_meteo import (
    DailyParameters,
    HourlyParameters,
    OpenMeteo,
    TemperatureUnit,
    WindSpeedUnit,
)

async with OpenMeteo() as open_meteo:
    forecast = await open_meteo.forecast(
        latitude=52.27,
        longitude=6.87417,
        timezone="Europe/Amsterdam",
        current=[
            HourlyParameters.TEMPERATURE_2M,
            HourlyParameters.WEATHER_CODE,
            HourlyParameters.IS_DAY,
        ],
        hourly=[
            HourlyParameters.TEMPERATURE_2M,
            HourlyParameters.PRECIPITATION_PROBABILITY,
        ],
        daily=[
            DailyParameters.TEMPERATURE_2M_MAX,
            DailyParameters.SUNRISE,
            DailyParameters.SUNSET,
        ],
        forecast_days=3,
        temperature_unit=TemperatureUnit.FAHRENHEIT,
        wind_speed_unit=WindSpeedUnit.METERS_PER_SECOND,
    )

    # Current conditions, with their units
    current = forecast.current
    print(current.temperature_2m, forecast.current_units.temperature_2m)

    # Hourly and daily data are lists, aligned with their time list
    for time, temperature in zip(
        forecast.hourly.time, forecast.hourly.temperature_2m, strict=True
    ):
        print(time, temperature)

    for day, sunrise in zip(forecast.daily.time, forecast.daily.sunrise, strict=True):
        print(day, sunrise)

Leave out forecast_days to get the API default of 7 days (up to 16), and use past_days to include data from the past as well.

By default, the API picks the best weather models for the location. Pick them yourself with models. With a single model, the data stays where it is. With multiple models, the data of each model ends up in a forecast of its own, in forecast.models, while the current conditions stay on the forecast itself:

from open_meteo import HourlyParameters, OpenMeteo

async with OpenMeteo() as open_meteo:
    forecast = await open_meteo.forecast(
        latitude=52.27,
        longitude=6.87417,
        hourly=[HourlyParameters.TEMPERATURE_2M],
        models=["icon_seamless", "gfs_seamless"],
    )

    for name, model in forecast.models.items():
        print(name, model.hourly.temperature_2m)

When only one of the models has data for the location, the API returns its data without telling which model it is from. It then stays on the forecast itself, like with a single model, and forecast.models is None.

Weather higher up in the atmosphere is available on pressure levels, like 850 or 500 hPa. Those end up per level, for the hourly data by default, or for the current conditions and 15-minutely data with pressure_level_sections.

from open_meteo import ForecastSection, OpenMeteo, PressureLevelVariable

async with OpenMeteo() as open_meteo:
    forecast = await open_meteo.forecast(
        latitude=52.27,
        longitude=6.87417,
        pressure_level_variables=[PressureLevelVariable.TEMPERATURE],
        pressure_levels=[850, 500],
        pressure_level_sections=[ForecastSection.CURRENT, ForecastSection.HOURLY],
    )

    print(forecast.current.pressure_levels[850].temperature)
    print(forecast.hourly.pressure_levels[500].temperature)

Some models, like UKMO, DMI, and KNMI, also have data at heights above ground, like 300 or 1000 meters. Those work the same, with height_level_variables, height_levels, and height_level_sections, and end up per height in meters. Heights that are a variable of their own, like temperature_80m, stay there.

from open_meteo import HeightLevelVariable, OpenMeteo

async with OpenMeteo() as open_meteo:
    forecast = await open_meteo.forecast(
        latitude=51.5,
        longitude=-0.12,
        models=["ukmo_seamless"],
        height_level_variables=[HeightLevelVariable.WIND_SPEED],
        height_levels=[300, 1000],
    )

    print(forecast.hourly.height_levels[1000].wind_speed)

Ensemble forecast

Ensemble models run the same forecast many times, with slightly different starting conditions, to show how certain a forecast is. The regular values are the control run, and every other run is a member.

from open_meteo import HourlyParameters, OpenMeteo

async with OpenMeteo() as open_meteo:
    forecast = await open_meteo.ensemble(
        latitude=52.27,
        longitude=6.87417,
        models=["ecmwf_ifs025_ensemble"],
        hourly=[HourlyParameters.TEMPERATURE_2M],
    )

    print(forecast.hourly.temperature_2m)
    for number, member in forecast.hourly.members.items():
        print(number, member.temperature_2m)

The ensemble mean models, like ecmwf_ifs025_ensemble_mean, return the mean over all members. With spread=True, they return the spread as well, the standard deviation, in forecast.hourly.spread.

Seasonal forecast

The seasonal API forecasts up to seven months ahead. Besides 6-hourly and daily data with all ensemble members, it has weekly and monthly statistics, like how much warmer or colder than normal it will likely be.

from open_meteo import OpenMeteo, SeasonalMonthlyParameters, SeasonalWeeklyParameters

async with OpenMeteo() as open_meteo:
    seasonal = await open_meteo.seasonal(
        latitude=52.27,
        longitude=6.87417,
        weekly=[SeasonalWeeklyParameters.TEMPERATURE_2M_ANOMALY],
        monthly=[SeasonalMonthlyParameters.PRECIPITATION_ANOMALY],
    )

    print(seasonal.weekly.time, seasonal.weekly.temperature_2m_anomaly)
    print(seasonal.monthly.time, seasonal.monthly.precipitation_anomaly)

Previous model runs

The previous runs API shows what the weather models forecasted for each hour, one to seven days before it. previous_days[1] holds, for every hour, the forecast made about a day earlier, so one series combines several model runs. That shows how a forecast changed, or how accurate forecasts were. For one complete run of a model, use the single runs API instead.

from open_meteo import HourlyParameters, OpenMeteo

async with OpenMeteo() as open_meteo:
    forecast = await open_meteo.previous_runs(
        latitude=52.27,
        longitude=6.87417,
        previous_days=[1, 2],
        hourly=[HourlyParameters.TEMPERATURE_2M],
    )

    print(forecast.hourly.temperature_2m)
    print(forecast.hourly.previous_days[1].temperature_2m)

Single model runs

The single runs API returns the forecast of one specific run of a weather model, starting at the time it ran. The run is in UTC.

from datetime import UTC, datetime

from open_meteo import HourlyParameters, OpenMeteo

async with OpenMeteo() as open_meteo:
    forecast = await open_meteo.single_run(
        latitude=52.27,
        longitude=6.87417,
        run=datetime(2026, 9, 1, 0, 0, tzinfo=UTC),
        hourly=[HourlyParameters.TEMPERATURE_2M],
        models=["ecmwf_ifs"],
    )

    print(forecast.hourly.time[0], forecast.hourly.temperature_2m)

Satellite radiation

The satellite radiation API has the solar radiation measured by weather satellites, back to 1983, every 10 to 30 minutes. There is no data for North America yet; for a location without data, it raises an OpenMeteoError.

from open_meteo import HourlyParameters, OpenMeteo, TemporalResolution

async with OpenMeteo() as open_meteo:
    satellite = await open_meteo.satellite_radiation(
        latitude=52.27,
        longitude=6.87417,
        past_days=1,
        hourly=[HourlyParameters.SHORTWAVE_RADIATION],
        temporal_resolution=TemporalResolution.NATIVE,
    )

    print(satellite.hourly.time, satellite.hourly.shortwave_radiation)

Historical forecast

The historical forecast API archives the forecasts the weather models made in the past, back to 2016. It takes the same variables and models as the forecast, for a range of dates.

from datetime import date

from open_meteo import DailyParameters, OpenMeteo

async with OpenMeteo() as open_meteo:
    forecast = await open_meteo.historical_forecast(
        latitude=52.27,
        longitude=6.87417,
        start_date=date(2024, 1, 1),
        end_date=date(2024, 1, 7),
        daily=[DailyParameters.TEMPERATURE_2M_MAX],
    )

    print(forecast.daily.temperature_2m_max)

Historical weather

The historical weather API has the weather of the past, back to 1940, from reanalysis datasets like ERA5. Those combine weather observations and weather models into the best estimate of what the weather was.

from datetime import date

from open_meteo import DailyParameters, OpenMeteo

async with OpenMeteo() as open_meteo:
    weather = await open_meteo.historical_weather(
        latitude=52.27,
        longitude=6.87417,
        start_date=date(1953, 1, 31),
        end_date=date(1953, 2, 1),
        daily=[DailyParameters.WIND_GUSTS_10M_MAX],
    )

    print(weather.daily.wind_gusts_10m_max)

Climate projections

The climate API has climate projections from 1950 up to 2050, from high resolution climate models. These are meant for long term trends, like the change in temperature over decades, not for the weather of a specific day.

from datetime import date

from open_meteo import DailyParameters, OpenMeteo

async with OpenMeteo() as open_meteo:
    climate = await open_meteo.climate(
        latitude=52.27,
        longitude=6.87417,
        start_date=date(2050, 7, 1),
        end_date=date(2050, 7, 31),
        daily=[DailyParameters.TEMPERATURE_2M_MAX],
        models=["MRI_AGCM3_2_S"],
    )

    print(climate.daily.temperature_2m_max)

Marine

The marine API forecasts waves, swell, ocean currents, sea surface temperature, and sea level. It only has data at sea: by default, the nearest sea grid cell is used, so locations near the coast get data too. Further inland, all values are None.

from open_meteo import MarineDailyParameters, MarineParameters, OpenMeteo

async with OpenMeteo() as open_meteo:
    marine = await open_meteo.marine(
        latitude=53.0,
        longitude=4.0,
        current=[
            MarineParameters.WAVE_HEIGHT,
            MarineParameters.SEA_SURFACE_TEMPERATURE,
        ],
        daily=[MarineDailyParameters.WAVE_HEIGHT_MAX],
    )

    print(marine.current.wave_height, marine.current.sea_surface_temperature)
    print(marine.daily.wave_height_max)

River discharge

The flood API forecasts the daily river discharge of the river nearest to a location, from the Global Flood Awareness System (GloFAS). With ensemble, it returns all ensemble members as well.

from open_meteo import FloodParameters, OpenMeteo

async with OpenMeteo() as open_meteo:
    flood = await open_meteo.flood(
        latitude=51.84,
        longitude=6.11,
        daily=[
            FloodParameters.RIVER_DISCHARGE,
            FloodParameters.RIVER_DISCHARGE_MAX,
        ],
        ensemble=True,
    )

    print(flood.daily.river_discharge)
    print(flood.daily.members[1].river_discharge)

Air quality

Air quality works the same way, with its own set of variables. These include particulate matter, gases, pollen (Europe only), and both the European and US air quality indices.

from open_meteo import AirQualityParameters, OpenMeteo

async with OpenMeteo() as open_meteo:
    air_quality = await open_meteo.air_quality(
        latitude=52.27,
        longitude=6.87417,
        current=[
            AirQualityParameters.EUROPEAN_AQI,
            AirQualityParameters.PM2_5,
        ],
        hourly=[AirQualityParameters.BIRCH_POLLEN],
    )

    print(air_quality.current.european_aqi)
    print(air_quality.hourly.birch_pollen)

Geocoding

Search for a location by name or postal code. This is handy to find the coordinates and timezone to use with the other APIs.

from open_meteo import OpenMeteo

async with OpenMeteo() as open_meteo:
    geocoding = await open_meteo.geocoding(name="Enschede", count=3)

    for result in geocoding.results or []:
        print(result.name, result.country, result.latitude, result.longitude)

    # A result can be looked up again later, by its ID
    if geocoding.results:
        location = await open_meteo.geocoding_by_id(
            location_id=geocoding.results[0].geo_id,
        )
        print(location.name, location.timezone)

results is None when nothing matches. geocoding_by_id returns that single result.

Elevation

Look up the elevation of a location, in meters above sea level.

from open_meteo import OpenMeteo

async with OpenMeteo() as open_meteo:
    elevation = await open_meteo.elevation(latitude=52.27, longitude=6.87417)

    print(elevation.elevation[0])

Connection options

All constructor arguments are optional:

OpenMeteo(
    request_timeout=10,  # per-request timeout in seconds (default: 10)
)

You may also pass your own aiohttp.ClientSession via session=... to share a connection pool. The client leaves a session you pass in open, and only closes the one it created itself.

Error handling

from open_meteo import (
    OpenMeteo,
    OpenMeteoConnectionError,
    OpenMeteoError,
    OpenMeteoRateLimitError,
    OpenMeteoResponseError,
)

try:
    async with OpenMeteo() as open_meteo:
        await open_meteo.forecast(latitude=999, longitude=0)
except OpenMeteoConnectionError:
    # Timeouts, DNS failures, or any other connection problem
    ...
except OpenMeteoRateLimitError as err:
    # Too many requests; retry_after has the seconds to wait, if the API
    # said so
    print(err.retry_after)
except OpenMeteoResponseError as err:
    # The API rejected the request; reason tells you why, like:
    # "Latitude must be in range of -90 to 90°. Given: 999.0."
    print(err.status, err.reason)
except OpenMeteoError:
    # Anything else unexpected, like a response that couldn't be parsed
    ...

Every exception for a failed request or an unexpected response is a subclass of OpenMeteoError, so catching that alone handles all of them. Invalid combinations of arguments raise a ValueError before anything is requested, like only one of pressure_level_variables and pressure_levels. The request timeout covers the whole request, including reading the response.

Changelog & Releases

This repository keeps a change log using GitHub's releases functionality. The format of the log is based on Keep a Changelog.

Releases are based on Semantic Versioning, and use the format of MAJOR.MINOR.PATCH. In a nutshell, the version will be incremented based on the following:

  • MAJOR: Incompatible or major changes.
  • MINOR: Backwards-compatible new features and enhancements.
  • PATCH: Backwards-compatible bugfixes and package updates.

Contributing

This is an active open-source project. We are always open to people who want to use the code or contribute to it.

We've set up a separate document for our contribution guidelines.

Thank you for being involved! 😍

Setting up development environment

This Python project is fully managed using the Poetry dependency manager. But also relies on the use of NodeJS for certain checks during development.

You need at least:

  • Python 3.11+
  • Poetry
  • NodeJS 24+ (including NPM)

To install all packages, including all development requirements:

npm install
poetry install
poetry run prek install

As this repository uses the prek framework, all changes are linted and tested with each commit. You can run all checks and tests manually, using the following command:

poetry run prek run --all-files

To run just the Python tests:

poetry run pytest

Authors & contributors

The original setup of this repository is by Franck Nijhof.

For a full list of all authors and contributors, check the contributor's page.

Disclaimer

This project is an independent, community-driven effort. It is not affiliated with, endorsed by, or supported by Open-Meteo.

The free Open-Meteo API is for non-commercial use only, and its data is licensed under Attribution 4.0 International (CC BY 4.0). If you use this library, those terms apply to you as well. Read the Open-Meteo terms for the details, including the rate limits and the attribution requirements.

This library talks to the free API only. The commercial API, which needs an API key, is not supported.

License

MIT License

Copyright (c) 2021-2026 Franck Nijhof

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Metadata

Release files for open-meteo 1.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for open-meteo 1.0.1
File Size Uploaded
open_meteo-1.0.1.tar.gz 51.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for open-meteo 1.0.1
File Interpreter ABI Platform
open_meteo-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 102.9 kB

Release files / open_meteo-1.0.1.tar.gz

Download URL open_meteo-1.0.1.tar.gz
Size 51.7 kB
Tags Source
SHA-256 checksum
How to use checksums
6528aff6f15e13d6ac5594ce963cf971d8b883eb5d780fffa59fd2d8161ba430
BLAKE2b-256 checksum
How to use checksums
ada47e9ba48b776aca3de2bad38cb19baa378d3e41adff63704ca8081d1af4ec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / open_meteo-1.0.1-py3-none-any.whl

Download URL open_meteo-1.0.1-py3-none-any.whl
Size 51.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
75fcb3994cac078d704a882453f207b4ebeeed2768c4821351b8c97e66fe9020
BLAKE2b-256 checksum
How to use checksums
2f36ceb246b5be9a1a01dee9c8826326f67c8b2c82e2bd6ec199a21fce64676d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files

1.0.0

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page