Skip to content

Python API reference

Everything below is importable from the top-level package:

import foehn

Tabular datasets come back as Polars DataFrames; gridded datasets as xarray Datasets. Every function takes the dataset's short name — run foehn.list_datasets() or foehn list to see them. For worked examples, see the Python API guide and the gridded data guide.

Discovering datasets

list_datasets

list_datasets() -> list[dict]

Return metadata about all available datasets.

Each dict has keys: dataset, collection_id, category, subcategory, description, format, frequencies, time_slices.

Loading tabular data

load

load(dataset: str, *, station: str | list[str] | None = None, frequency: str | list[str] | None = None, time_slice: str | list[str] | None = None, year: int | list[int] | None = None, month: int | list[int] | None = None, date_from: str | None = None, date_to: str | None = None, columns: list[str] | None = None, drop_null: str | None = None, sort: str | None = None, limit: int | None = None, workers: int = 8) -> DataFrame

Load a dataset and return it as an in-memory Polars DataFrame.

No files are written to disk. Data is fetched from the MeteoSwiss STAC API, parsed directly in memory, and returned as a single concatenated DataFrame.

Parameters:

Name Type Description Default
dataset str

Dataset name (e.g. "smn"). Use list_datasets() to see options.

required
station str | list[str] | None

Station abbreviation(s) to include (e.g. "BER" or ["BER", "ZUR"]). Filters at the STAC item level so unmatched stations are never downloaded. Case-insensitive. If None, all stations are included.

None
frequency str | list[str] | None

Granularity filter(s). Options: "t" (10-minute, near real-time), "h" (hourly), "d" (daily), "m" (monthly), "y" (yearly). Can be a single string or list. If None, all are included.

None
time_slice str | list[str] | None

Time slice(s) to include. Options: "now" (last ~24 hours, updated every 10 minutes — t and h only), "recent" (this calendar year through yesterday, updated daily), "historical" (start of measurements through Dec 31 of last year). Defaults to ["recent"]. Can be a single string or list.

None
year int | list[int] | None

Filter to specific year(s) (e.g. 2025 or [2020, 2021, 2022]).

None
month int | list[int] | None

Filter to specific month(s) (1-12, e.g. 7 or [6, 7, 8] for summer).

None
date_from str | None

Start date (inclusive), ISO format "YYYY-MM-DD".

None
date_to str | None

End date (inclusive), ISO format "YYYY-MM-DD".

None
columns list[str] | None

Only return these columns. station_abbr and reference_timestamp are always included.

None
drop_null str | None

Drop rows where this column is null.

None
sort str | None

Sort by timestamp. Options: "asc" (oldest first) or "desc" (newest first).

None
limit int | None

Cap the returned DataFrame to N rows. Applied after sort/columns. NOTE: this only bounds the returned shape — it does not reduce network bytes, since CSVs are per-station and not pre-bucketed by row count. To reduce wire volume, narrow with time_slice="now", year, or date_from/date_to.

None
workers int

Concurrent CSV downloads (default 8). The CSV fetches are parallelised via a ThreadPoolExecutor; metadata fetch stays serial.

8

Returns:

Type Description
DataFrame

A Polars DataFrame containing all matching CSV data.

Example::

import foehn

# Recent daily data for Bern
df = foehn.load("smn", station="BER", frequency="d")

# Hourly data for multiple stations
df = foehn.load("smn", station=["BER", "ZUR"], frequency="h")

# Daily data for Bern, only January 2026
df = foehn.load("smn", station="BER", frequency="d", year=2026, month=1)

# Latest 10 readings (sort+limit are applied after the CSV is parsed)
df = foehn.load("smn", station="BER", frequency="t",
                time_slice="now", sort="desc", limit=10)

# Summer 2025 temperatures, sorted newest first
df = foehn.load("smn", station="BER", frequency="d",
                time_slice="historical", date_from="2025-06-01",
                date_to="2025-08-31", columns=["tre200d0"],
                sort="desc")

Metadata

parameters

parameters(dataset: str) -> DataFrame

Fetch parameter metadata for a dataset.

Returns a DataFrame with columns: shortname, description, unit, type, granularity, decimals, group.

Parameters:

Name Type Description Default
dataset str

Dataset name (e.g. "smn"). Use list_datasets() to see options.

required

stations

stations(dataset: str) -> DataFrame

Fetch station metadata for a dataset.

Returns a DataFrame with columns: abbr, name, canton, altitude, lv95_east, lv95_north, lat, lon, data_since. data_since keeps the MeteoSwiss station metadata format (DD.MM.YYYY).

Parameters:

Name Type Description Default
dataset str

Dataset name (e.g. "smn"). Use list_datasets() to see options.

required

points

points(dataset: str) -> DataFrame

Fetch forecast point metadata for a dataset (forecast_local).

Local forecasts are published per point rather than per station: every station, plus one point per postal code centre and some points of interest. Returns a DataFrame with columns: point_id, point_type_id, type, abbr, postal_code, name, altitude, lv95_east, lv95_north, lat, lon. abbr is null unless the point is a station, postal_code unless it is a postal code centre.

Parameters:

Name Type Description Default
dataset str

Dataset name (e.g. "forecast_local").

required

inventory

inventory(dataset: str) -> DataFrame

Fetch the data inventory for a dataset.

Returns a DataFrame with columns: station, parameter, data_since, data_till, owner.

Parameters:

Name Type Description Default
dataset str

Dataset name (e.g. "smn"). Use list_datasets() to see options.

required

metadata

metadata(dataset: str, table: str) -> DataFrame

Fetch one of a dataset's metadata tables from the STAC API.

The single implementation behind :func:parameters, :func:stations, :func:points and :func:inventory; take it directly when the table is chosen at runtime, as the CLI does.

Parameters:

Name Type Description Default
dataset str

Dataset name (e.g. "smn"). Use list_datasets() to see options.

required
table str

One of METADATA_TABLES — "parameters", "stations", "points", "inventory".

required

Returns:

Type Description
DataFrame

A Polars DataFrame with foehn's published column names.

Downloading and converting

download

download(dataset: str, *, data_dir: Path | str | None = None, time_slice: str | list[str] | None = None, since: str | None = None, workers: int = 8, force: bool = False) -> DownloadResult

Download a single dataset.

Parameters:

Name Type Description Default
dataset str

Dataset name (e.g. "smn"). Use list_datasets() to see options.

required
data_dir Path | str | None

Root data directory. Defaults to ./data/meteoswiss.

None
time_slice str | list[str] | None

Time slice(s) to download. Options: "now" (last ~24 hours, updated every 10 minutes — t and h only), "recent" (this calendar year through yesterday, updated daily), "historical" (start of measurements through Dec 31 of last year). Defaults to ["recent"]. Can be a single string or list. Ignored for binary/grid datasets: NetCDF and radar fetch what the collection holds, and GRIB2 forecasts fetch the control member of the newest complete run (~8 GB for ICON-CH1).

None
since str | None

ISO timestamp for incremental updates — only assets updated after it are fetched. This function does not track state itself: pass the previous run's timestamp, or use the foehn download CLI, which keeps one per dataset and time slice in _last_run.json and only advances a dataset's when that dataset fully succeeds. Must carry a timezone; it is compared as an instant, not as text.

None
workers int

Concurrent HTTP downloads (default 8).

8
force bool

Re-download every selected file even when the local copy looks up to date: no freshness check, and no stored ETag sent, so a damaged file in the cache is replaced. since still narrows which files are selected.

False

Returns:

Type Description
DownloadResult

DownloadResult summarising the download. Use result.downloaded > 0

DownloadResult

to gate expensive downstream processing and result.failed to detect

DownloadResult

partial failures.

DownloadResult

DownloadResult(total_assets: int = 0, downloaded: int = 0, skipped: int = 0, failed: int = 0, filenames: list[str] = <factory>)

Summary of a download call.

Callers use this to decide whether to run expensive downstream work (e.g. Spark MERGE INTO) without scanning the output directory.

downloaded + skipped + failed == total_assets always holds: total_assets is every asset handed to :func:fetch_all, skipped counts both the ones skip rejected locally and the ones the server answered 304 for, and destination collisions count as skipped too.

to_parquet

to_parquet(dataset: str, *, data_dir: Path | str | None = None) -> None

Convert downloaded CSVs to Parquet for a single dataset.

Parameters:

Name Type Description Default
dataset str

Dataset name (e.g. "smn").

required
data_dir Path | str | None

Root data directory. Defaults to ./data/meteoswiss.

None

Raises:

Type Description
RuntimeError

If one or more groups fail to convert. Each failure is also logged as a warning on the foehn logger, naming the group and the cause. Mirrors the CLI's exit-1 semantics so Python callers can't accidentally treat partial output as success.

Gridded data

open_dataset

open_dataset(dataset: str, *, variables: str | list[str] | None = None, match: str | None = None, data_dir: Path | str | None = None, engine: str | None = None) -> Dataset

Open a gridded dataset as an xarray Dataset.

The grid analog of foehn.load(), for NetCDF collections (climate grids, normals, scenarios), GRIB2 forecasts (ICON-CH1/CH2, KENDA), and HDF5/ODIM radar composites (CombiPrecip, hail). This is download-then-lazy: the source file(s) are fetched in full to data_dir/bronze/<dataset>/ on first use, then opened and read from that local copy. It is not cloud-lazy — there is no byte-range/partial read of the remote file, so the first call pays the full file size up front. Subsequent calls reuse the cache. A match spanning several NetCDF files stays lazy only with dask installed (it is not part of the 'grids' extra): without it, combining the files reads their values into memory on open, and foehn warns when that is over 1 GB.

GRIB2 and radar (HDF5) collections require match, and it must resolve to a single file:

  • GRIB2 forecast collections hold thousands of files (one per variable × ensemble member × lead time × reference time), and ICON's native unstructured (icosahedral) grid — a 1-D values dimension with no dimension coordinate — can't be stacked by combine_by_coords. Include the reference + lead time, e.g. match="YYYYMMDDHHMM-0-t_2m-ctrl". The one field comes back on the values grid with cell lat/lon coordinates joined from the collection's horizontal-constants file.
  • Radar collections hold one Cartesian composite per timestep (every ~5 min). Match a single file, e.g. match="cpcYYDDDHHMM". The composite is read with ODIM gain/offset scaling, nodata masked to NaN, on Swiss LV95 x/y coordinates (matching the NetCDF grids).

open_dataset reads one field; to assemble many matched files into a cube use to_zarr(..., stack=True) instead.

Parameters:

Name Type Description Default
dataset str

Dataset name (e.g. "surface_derived_grid", "forecast_icon_ch1", "radar_precip"). Use list_datasets() to see options. Must be a NetCDF, GRIB2, or HDF5/radar collection.

required
variables str | list[str] | None

Restrict to these data variable(s). If None, all are kept.

None
match str | None

Keep only source files whose name contains this substring. Narrows a heterogeneous multi-file collection to one coherent set — analogous to the station/frequency filters on load(). Required for GRIB2 and radar collections, where it must select a single file.

None
data_dir Path | str | None

Root data directory. Defaults to ./data/meteoswiss.

None
engine str | None

xarray backend to open the file(s) with (e.g. "h5netcdf"). Leave unset to let xarray choose, which is right for every published collection; name one only to work around a backend-specific fault.

None

Returns:

Type Description
Dataset

An xarray Dataset backed by the local file(s), downloaded in full first

Dataset

(see the download-then-lazy note above) — e.g. the first

Dataset

climate_scenarios_grid call fetches ~900 MB before you read a pixel.

Raises:

Type Description
ValueError

If the dataset is unknown, tabular (CSV), a GRIB2/radar collection opened without a single-file match, if the match selects more files than the kind will open at once, or if its files cannot be combined into a single Dataset (narrow it with match).

OSError

If one of the matched files cannot be read — a corrupt entry in the local cache. The message names the file; delete it and retry.

ImportError

If the optional 'grids' dependencies are not installed. Raised before anything is downloaded.

Example::

import foehn

# NetCDF: a coherent single-parameter slice of a multi-file collection
ds = foehn.open_dataset("surface_derived_grid", match="rhiresd")
ds = foehn.open_dataset("climate_scenarios_grid", match="_pr_", variables="pr")

# GRIB2: a single forecast field — variable + member + reference + lead time
ds = foehn.open_dataset("forecast_icon_ch1", match="YYYYMMDDHHMM-0-t_2m-ctrl")

# Radar: a single CombiPrecip composite (one 5-min timestep)
ds = foehn.open_dataset("radar_precip", match="cpcYYDDDHHMM")

to_zarr

to_zarr(dataset: str, *, variables: str | list[str] | None = None, match: str | None = None, data_dir: Path | str | None = None, store: Path | str | None = None, rechunk: dict[str, int] | None = None, mode: str = 'w', stack: bool | str | None = False) -> Path

Materialise a gridded dataset to a Zarr store on disk.

The grid analog of foehn.to_parquet(): reads the source (NetCDF, GRIB2, or HDF5/radar) via open_dataset() and writes a single Zarr store under data_dir/zarr/. (GRIB2 and radar collections require match — see open_dataset.)

The default store name encodes match so that different filtered slices of the same collection don't silently overwrite each other: <dataset>.zarr when unfiltered, <dataset>__<match>.zarr otherwise (e.g. surface_derived_grid__rhiresd.zarr). Pass store for an explicit path that overrides this.

Parameters:

Name Type Description Default
dataset str

Dataset name. Must be a NetCDF, GRIB2, or HDF5/radar collection.

required
variables str | list[str] | None

Restrict to these data variable(s) before writing.

None
match str | None

Narrow a multi-file collection to a coherent set (see open_dataset); required for GRIB2 and radar collections, and for stack.

None
data_dir Path | str | None

Root data directory. Defaults to ./data/meteoswiss.

None
store Path | str | None

Explicit output path for the .zarr store. Overrides the derived data_dir/zarr/<name>.zarr location when given.

None
rechunk dict[str, int] | None

Optional dim→chunk-size mapping applied before writing, e.g. {"time": 24}. Requires dask (not part of the 'grids' extra — install separately with pip install dask); raises ImportError if it is missing. Not supported together with stack.

None
mode str

Zarr write mode (default "w" — overwrite the store at this path). Note distinct match values map to distinct default paths, so this only overwrites a prior run of the same slice, not a different one. Replacement mode is staged; "a" writes in place so its work and temporary disk use scale with the new data, without rollback on interruption.

'w'
stack bool | str | None

Assemble the matched files into one cube, using whichever method the dataset's kind uses — radar stacks CombiPrecip timesteps into a (time, y, x) cube incrementally (dask-free, one timestep in memory); GRIB2 promotes whichever of number/time/step vary into an N-D cube (e.g. (time, step, values)) via combine_by_coords (whole set in memory, capped at 1000 files). NetCDF has no cube builder — a multi-file match already combines on read — so stack is a no-op there. Incompatible with rechunk.

False

Returns:

Type Description
Path

Path to the written .zarr store.

Diagnostics

show_versions

show_versions() -> None

Print foehn's version, the platform and every dependency's version.

Paste the output into a bug report::

import foehn

foehn.show_versions()

The command line equivalent is foehn version.