Skip to content

owid-catalog: Data APIs

The Data API provides unified access to OWID's published data through a simple client interface.

Quick Reference

The API library is centered around the Client class, which provides quick access to different data APIs: IndicatorsAPI, TablesAPI, and ChartsAPI. Each API provides methods search() and fetch() for discovering and retrieving data, respectively.

For example to fetch a table by its path:

from owid.catalog import Client

client = Client()
tb = client.tables.fetch("garden/un/2024-07-12/un_wpp/population")

For convenience, the library provides functions for the most common use cases:

from owid.catalog import search, fetch

# Search for charts (default)
results = search("population")
tb = results[0].fetch()

# Direct fetch (by chart slug or table path)
tb = fetch("life-expectancy")
tb = fetch("garden/un/2024-07-12/un_wpp/population")

Lazy Loading

All fetch() methods return Table-like objects, which resemble pandas.DataFrame with the addition of metadata attributes that describe the data.

tb = client.charts.fetch("life-expectancy")
tb.metadata  # Available immediately
tb["life_expectancy_0"].metadata  # Column metadata available

Optionally, you can defer data loading until it's actually needed, by using the load_data=False parameter in fetch() methods.

Path Formats

Different APIs use different path conventions:

  • Charts: "life-expectancy" (simple slug), "years-of-schooling?metric_type=expected_years_schooling&level=primary&sex=boys" (with query params), or "https://ourworldindata.org/grapher/life-expectancy" (full URL)
  • Tables: "garden/un/2024-07-12/un_wpp/population" (channel/namespace/version/dataset/table)
  • Indicators: "garden/un/2024-07-12/un_wpp/population#population" (table path + #column)

API Reference

owid.catalog.api.quick

Quick access functions for data discovery and retrieval.

For more complex use cases, refer to the full API.

This module provides two convenience functions that separate discovery from download:

  • search(): Browse available data without downloading anything.
  • fetch(): Download data by slug, URL, or catalog path.
Search for available data (no download)
>>> from owid.catalog import search
>>> results = search("population")  # Returns ResponseSet[ChartResult] (default)
>>> print(f"Found {len(results)} charts")
>>> print(results[0].slug)
Fetch specific data by path
>>> from owid.catalog import fetch
>>> tb = fetch("life-expectancy")  # Chart slug auto-detected
>>> tb = fetch("years-of-schooling?metric_type=expected_years_schooling&level=primary&sex=boys")  # Slug with query params
>>> tb = fetch("https://ourworldindata.org/grapher/life-expectancy")  # Full URL
>>> tb = fetch("garden/un/2024-07-12/un_wpp/population")  # Table path
>>> tb_ind = fetch("garden/un/2024-07-12/un_wpp/population#population")  # Indicator

Functions:

  • search

    Search for available data without downloading (for browsing/discovery).

  • fetch

    Fetch data directly by path (auto-detects tables, indicators, or charts).

search

search(
    name: str | None = None,
    *,
    kind: Literal["table", "indicator", "chart"] = "chart",
    limit: int = 10,
    namespace: str | None = None,
    version: str | None = None,
    dataset: str | None = None,
    channel: str | None = None,
    match: Literal[
        "exact", "contains", "regex", "fuzzy"
    ] = "fuzzy",
    fuzzy_threshold: int = 70,
    case: bool = False,
    latest: bool = False,
) -> (
    ResponseSet[TableResult]
    | ResponseSet[IndicatorResult]
    | ResponseSet[ChartResult]
)

Search for available data without downloading (for browsing/discovery).

This function searches for data in the catalog and returns a ResponseSet of results without downloading the actual data. Use this to explore and find the exact path or slug, then use fetch() to download the data.

Parameters:

  • name (str | None, default: None ) –

    Name or pattern to search for (e.g., "population", "gdp", "life-expectancy"). Required for indicators and charts. Optional for tables (can filter by other params).

  • kind (Literal['table', 'indicator', 'chart'], default: 'chart' ) –

    What to search for (default: "chart"):

    • "chart": Search published charts (returns ResponseSet[ChartResult])
    • "table": Search catalog tables (returns ResponseSet[TableResult])
    • "indicator": Search indicators/variables (returns ResponseSet[IndicatorResult])
  • limit (int, default: 10 ) –

    Maximum number of results to return (default: 10)

  • namespace (str | None, default: None ) –

    Filter by namespace (e.g., "un", "worldbank"). Only for tables.

  • version (str | None, default: None ) –

    Filter by specific version (e.g., "2024-01-15"). Only for tables.

  • dataset (str | None, default: None ) –

    Filter by dataset name. Only for tables.

  • channel (str | None, default: None ) –

    Filter by channel (e.g., "garden", "grapher"). Only for tables, and name field.

  • match (Literal['exact', 'contains', 'regex', 'fuzzy'], default: 'fuzzy' ) –

    Matching mode (default: "fuzzy" for typo-tolerance) (only for tables, and name field):

    • "fuzzy": Typo-tolerant similarity matching
    • "exact": Exact string match
    • "contains": Substring match
    • "regex": Regular expression
  • fuzzy_threshold (int, default: 70 ) –

    Minimum similarity score 0-100 for fuzzy matching (default: 70). Only for tables, and name field.

  • case (bool, default: False ) –

    Case-sensitive search (default: False). Only for tables.

  • latest (bool, default: False ) –

    If True, keep only the latest version of each result (grouped by namespace/dataset/table or indicator). Only for tables and indicators. Note: results without a version are dropped when this is enabled.

Returns:

Example
# Search for charts (default)
results = search("population")
print(f"Found {len(results)} charts")
print(results[0].slug)  # Access chart slug without downloading data

# Search for tables
results = search("population", kind="table")
print(results[0].path)

# Search for indicators
results = search("gdp", kind="indicator")
print(results[0].title)

# Exact match for tables
results = search("population", kind="table", match="exact")

# Filter tables by namespace and version
results = search("wdi", kind="table", namespace="worldbank_wdi", version="2024-01-10")

# Then fetch the data you need:
tb = results[0].fetch()
Warning

For indicators and charts, filtering parameters (namespace, version, dataset, channel) are ignored as they don't apply to those search types.

Source code in lib/catalog/owid/catalog/api/quick.py
def search(
    name: str | None = None,
    *,
    kind: Literal["table", "indicator", "chart"] = "chart",
    limit: int = 10,
    namespace: str | None = None,
    version: str | None = None,
    dataset: str | None = None,
    channel: str | None = None,
    match: Literal["exact", "contains", "regex", "fuzzy"] = "fuzzy",
    fuzzy_threshold: int = 70,
    case: bool = False,
    latest: bool = False,
) -> ResponseSet[TableResult] | ResponseSet[IndicatorResult] | ResponseSet[ChartResult]:
    """Search for available data without downloading (for browsing/discovery).

    This function searches for data in the catalog and returns a ResponseSet of results
    without downloading the actual data. Use this to explore and find the exact path or
    slug, then use fetch() to download the data.

    Args:
        name: Name or pattern to search for (e.g., "population", "gdp", "life-expectancy").
            Required for indicators and charts. Optional for tables (can filter by other params).
        kind: What to search for (default: "chart"):

            - "chart": Search published charts (returns ResponseSet[ChartResult])
            - "table": Search catalog tables (returns ResponseSet[TableResult])
            - "indicator": Search indicators/variables (returns ResponseSet[IndicatorResult])
        limit: Maximum number of results to return (default: 10)
        namespace: Filter by namespace (e.g., "un", "worldbank"). Only for tables.
        version: Filter by specific version (e.g., "2024-01-15"). Only for tables.
        dataset: Filter by dataset name. Only for tables.
        channel: Filter by channel (e.g., "garden", "grapher"). Only for tables, and `name` field.
        match: Matching mode (default: "fuzzy" for typo-tolerance) (only for tables, and `name` field):

            - "fuzzy": Typo-tolerant similarity matching
            - "exact": Exact string match
            - "contains": Substring match
            - "regex": Regular expression
        fuzzy_threshold: Minimum similarity score 0-100 for fuzzy matching (default: 70).  Only for tables, and `name` field.
        case: Case-sensitive search (default: False).  Only for tables.
        latest: If True, keep only the latest version of each result
            (grouped by namespace/dataset/table or indicator). Only for tables and indicators.
            Note: results without a version are dropped when this is enabled.

    Returns:
        Search results. Results can be indexed, iterated, and provide access to metadata without downloading data.

    Example:
        ```python
        # Search for charts (default)
        results = search("population")
        print(f"Found {len(results)} charts")
        print(results[0].slug)  # Access chart slug without downloading data

        # Search for tables
        results = search("population", kind="table")
        print(results[0].path)

        # Search for indicators
        results = search("gdp", kind="indicator")
        print(results[0].title)

        # Exact match for tables
        results = search("population", kind="table", match="exact")

        # Filter tables by namespace and version
        results = search("wdi", kind="table", namespace="worldbank_wdi", version="2024-01-10")

        # Then fetch the data you need:
        tb = results[0].fetch()
        ```

    Warning:
        For indicators and charts, filtering parameters (namespace, version, dataset, channel)
        are ignored as they don't apply to those search types.
    """
    # Validate name is provided for indicators and charts
    if name is None and kind in ("indicator", "chart"):
        raise ValueError(f"'name' is required when searching for {kind}s.")

    # Route to appropriate search method based on kind
    client = Client()

    if kind == "table":
        # Search tables using TablesAPI
        return client.tables.search(
            table=name,
            namespace=namespace,
            version=version,
            dataset=dataset,
            channel=channel,
            match=match,
            fuzzy_threshold=fuzzy_threshold,
            case=case,
            latest=latest,
        )
    elif kind == "indicator":
        # Search indicators using IndicatorsAPI
        assert name is not None  # Validated above
        return client.indicators.search(name, limit=limit, latest=latest)
    elif kind == "chart":
        # Search charts using ChartsAPI
        assert name is not None  # Validated above
        return client.charts.search(name, limit=limit)
    else:
        raise ValueError(f"Invalid kind='{kind}'. Must be 'table', 'indicator', or 'chart'.")

fetch

fetch(path: str) -> Table | ChartTable

Fetch data directly by path (auto-detects tables, indicators, or charts).

This function downloads the data associated with the given path. It auto-detects whether you're accessing a table, indicator, or chart based on the path format.

Parameters:

  • path (str) –

    Path to the data resource:

    • Table: "channel/namespace/version/dataset/table"
    • Indicator: "channel/namespace/version/dataset/table#variable"
    • Chart slug: "life-expectancy"
    • Chart URL: "https://ourworldindata.org/grapher/life-expectancy"
    • Chart slug with query params: "years-of-schooling?metric_type=expected_years_schooling&level=primary&sex=boys"
    • Explorer URL: "https://ourworldindata.org/explorers/energy"

Returns:

  • Table | ChartTable

    Table (for tables or indicators) or CharTable (for charts)

Raises:

  • ValueError

    If path format is invalid or resource not found

Example
# Fetch table
tb = fetch("garden/un/2024-07-12/un_wpp/population")
print(tb.shape)
print(tb.metadata)

# Fetch indicator as Table (single column)
tb = fetch("garden/un/2024-07-12/un_wpp/population#population")
print(tb.columns)

# Fetch chart data (slug auto-detected)
tb = fetch("life-expectancy")
print(tb.metadata.title)

# Fetch chart with query params
tb = fetch("years-of-schooling?metric_type=expected_years_schooling&level=primary&sex=boys")

# Fetch chart by full URL
tb = fetch("https://ourworldindata.org/grapher/life-expectancy")

# Fetch from grapher channel
tb = fetch("grapher/demography/2025-10-22/life_expectancy/life_expectancy_at_birth")
Source code in lib/catalog/owid/catalog/api/quick.py
def fetch(path: str) -> Table | ChartTable:
    """Fetch data directly by path (auto-detects tables, indicators, or charts).

    This function downloads the data associated with the given path. It auto-detects
    whether you're accessing a table, indicator, or chart based on the path format.

    Args:
        path: Path to the data resource:

            - Table: "channel/namespace/version/dataset/table"
            - Indicator: "channel/namespace/version/dataset/table#variable"
            - Chart slug: "life-expectancy"
            - Chart URL: "https://ourworldindata.org/grapher/life-expectancy"
            - Chart slug with query params: "years-of-schooling?metric_type=expected_years_schooling&level=primary&sex=boys"
            - Explorer URL: "https://ourworldindata.org/explorers/energy"

    Returns:
        Table (for tables or indicators) or CharTable (for charts)

    Raises:
        ValueError: If path format is invalid or resource not found

    Example:
        ```python
        # Fetch table
        tb = fetch("garden/un/2024-07-12/un_wpp/population")
        print(tb.shape)
        print(tb.metadata)

        # Fetch indicator as Table (single column)
        tb = fetch("garden/un/2024-07-12/un_wpp/population#population")
        print(tb.columns)

        # Fetch chart data (slug auto-detected)
        tb = fetch("life-expectancy")
        print(tb.metadata.title)

        # Fetch chart with query params
        tb = fetch("years-of-schooling?metric_type=expected_years_schooling&level=primary&sex=boys")

        # Fetch chart by full URL
        tb = fetch("https://ourworldindata.org/grapher/life-expectancy")

        # Fetch from grapher channel
        tb = fetch("grapher/demography/2025-10-22/life_expectancy/life_expectancy_at_birth")
        ```
    """
    # Create client (reuses singleton internally)
    client = Client()

    # Detect path type based on structure:
    # - Starts with "https://" → full chart/explorer URL
    # - Contains "/" or "#" (but not a URL) → catalog path (table or indicator)
    # - Matches slug pattern (possibly with ?query params) → chart slug

    if path.startswith("https://"):
        # Full URL — route to charts (handles grapher + explorer URLs)
        return client.charts.fetch(path)

    elif "/" in path or "#" in path:
        # Catalog path (table or indicator)
        try:
            catalog_path = CatalogPath.from_str(path)

            if catalog_path.variable is not None:
                # Indicator path (table path with #variable fragment)
                # Fetch using IndicatorsAPI which returns Table
                return client.indicators.fetch(path)
            else:
                # Regular table path
                return client.tables.fetch(path)

        except ValueError as e:
            # Re-raise with more context
            raise ValueError(
                f"Invalid catalog path: '{path}'. "
                f"Expected format: 'channel/namespace/version/dataset/table' "
                f"or 'channel/namespace/version/dataset/table#variable'. "
                f"Error: {e}"
            ) from e

    elif _CHART_SLUG_PATTERN.match(path.split("?")[0]):
        # Chart slug, possibly with query params (e.g. "life-expectancy?country=USA")
        return client.charts.fetch(path)

    else:
        raise ValueError(
            f"Invalid path format: '{path}'. "
            f"Expected a catalog path (with '/'), indicator path (with '#'), "
            f"or chart slug (alphanumeric with dashes/underscores, optionally with ?query params). If passing a URL for a chart or explorer, ensure it starts with 'https://'."
        )

owid.catalog.api.Client

Client(
    timeout: int = 30,
    catalog_url: str = DEFAULT_CATALOG_URL,
    site_url: str = DEFAULT_SITE_URL,
    indicators_search_url: str = DEFAULT_INDICATORS_SEARCH_URL,
    site_search_url: str = DEFAULT_SITE_SEARCH_URL,
)

Unified client for all OWID data APIs.

Provides access to our main APIs:

  • ChartsAPI: Fetch and search for published charts
  • IndicatorsAPI: Semantic search for data indicators
  • TablesAPI: Query and load tables from the data catalog

Attributes:

  • charts (ChartsAPI) –

    ChartsAPI instance for chart operations and search.

  • indicators (IndicatorsAPI) –

    IndicatorsAPI instance for indicator search.

  • tables (TablesAPI) –

    TablesAPI instance for catalog operations.

Example
from owid.catalog import Client

client = Client()

# Charts: Published visualizations
results = client.charts.search("climate change")
chart = client.charts.fetch("life-expectancy")

# Tables: Catalog datasets
results = client.tables.search(table="population", namespace="un")
tb = client.tables.fetch("garden/un/2024-07-12/un_wpp/population")

# Indicators: Semantic search for data series
results = client.indicators.search("renewable energy")
variable = client.indicators.fetch("garden/un/2024-07-12/un_wpp/population#population")

# Custom URLs (e.g., for staging environments)
staging_client = Client(catalog_url="https://staging-catalog.example.com/")

Initialize the client with all API interfaces.

Parameters:

  • timeout (int, default: 30 ) –

    HTTP request timeout in seconds. Default 30.

  • catalog_url (str, default: DEFAULT_CATALOG_URL ) –

    Base URL for the catalog. Default: https://catalog.ourworldindata.org/

  • site_url (str, default: DEFAULT_SITE_URL ) –

    Base URL for the OWID website. Default: https://ourworldindata.org

  • indicators_search_url (str, default: DEFAULT_INDICATORS_SEARCH_URL ) –

    URL for indicators search API. Default: https://search.owid.io/indicators

  • site_search_url (str, default: DEFAULT_SITE_SEARCH_URL ) –

    URL for site search API. Default: https://ourworldindata.org/api/search

Source code in lib/catalog/owid/catalog/api/client.py
def __init__(
    self,
    timeout: int = 30,
    catalog_url: str = DEFAULT_CATALOG_URL,
    site_url: str = DEFAULT_SITE_URL,
    indicators_search_url: str = DEFAULT_INDICATORS_SEARCH_URL,
    site_search_url: str = DEFAULT_SITE_SEARCH_URL,
) -> None:
    """Initialize the client with all API interfaces.

    Args:
        timeout: HTTP request timeout in seconds. Default 30.
        catalog_url: Base URL for the catalog. Default: https://catalog.ourworldindata.org/
        site_url: Base URL for the OWID website. Default: https://ourworldindata.org
        indicators_search_url: URL for indicators search API. Default: https://search.owid.io/indicators
        site_search_url: URL for site search API. Default: https://ourworldindata.org/api/search
    """
    self.timeout = timeout
    self._site_url = site_url
    self._datasette = DatasetteAPI(timeout=timeout)
    self.charts = ChartsAPI(self, site_url=site_url)
    self.indicators = IndicatorsAPI(self, search_url=indicators_search_url, catalog_url=catalog_url)
    self.tables = TablesAPI(self, catalog_url=catalog_url)
    self._site_search = SiteSearchAPI(self, base_url=site_search_url, site_url=site_url)

owid.catalog.api.charts.ChartsAPI

ChartsAPI(client: Client, site_url: str)

API for accessing OWID chart data and metadata.

Provides methods to fetch data and metadata from published charts on ourworldindata.org. Also includes search functionality to find charts by keywords.

Example
from owid.catalog import Client

client = Client()

# Fetch chart data as ChartTable
tb = client.charts.fetch("life-expectancy")
print(tb.head())
print(tb["life_expectancy_0"].metadata.unit)
print(tb.metadata.chart_config.get("title"))  # Access chart config

# Search for charts
results = client.charts.search("gdp per capita")
tb = results[0].fetch()  # Fetch chart data as ChartTable

Initialize the ChartsAPI.

Parameters:

  • client (Client) –

    The Client instance.

  • site_url (str) –

    Base URL for the OWID website (e.g., "https://ourworldindata.org").

Methods:

  • search

    Search for charts matching a query.

  • fetch

    Fetch chart data as a ChartTable with rich metadata.

Attributes:

  • base_url (str) –

    Base URL for the Grapher (read-only).

Source code in lib/catalog/owid/catalog/api/charts.py
def __init__(self, client: Client, site_url: str) -> None:
    """Initialize the ChartsAPI.

    Args:
        client: The Client instance.
        site_url: Base URL for the OWID website (e.g., "https://ourworldindata.org").
    """
    self._client = client
    self._site_url = site_url

base_url property

base_url: str

Base URL for the Grapher (read-only).

search

search(
    query: str,
    *,
    countries: list[str] | None = None,
    topics: list[str] | None = None,
    require_all_countries: bool = False,
    limit: int = 10,
    page: int = 0,
    timeout: int | None = None,
) -> ResponseSet[ChartResult]

Search for charts matching a query.

Parameters:

  • query (str) –

    Search query string.

  • countries (list[str] | None, default: None ) –

    Optional list of country names to filter by.

  • topics (list[str] | None, default: None ) –

    Optional list of topic names to filter by.

  • require_all_countries (bool, default: False ) –

    If True, only return charts with ALL specified countries. Default False (any country matches).

  • limit (int, default: 10 ) –

    Maximum results to return (1-100). Default 20.

  • page (int, default: 0 ) –

    Page number for pagination (0-indexed). Default 0.

  • timeout (int | None, default: None ) –

    HTTP request timeout in seconds. Defaults to client timeout.

Returns:

  • ResponseSet[ChartResult]

    ResponseSet containing ChartResult objects, sorted by popularity (most viewed first).

  • ResponseSet[ChartResult]

    Each result includes a popularity field (0.0-1.0) based on analytics views.

Example
# Basic search (sorted by popularity)
results = client.charts.search("life expectancy")
for chart in results:
    print(f"{chart.title}: popularity={chart.popularity:.3f}")

# Filter by countries
results = client.charts.search(
    "gdp",
    countries=["France", "Germany"],
    require_all_countries=True
)

# Get data from search results
tb = results[0].fetch()
Source code in lib/catalog/owid/catalog/api/charts.py
def search(
    self,
    query: str,
    *,
    countries: list[str] | None = None,
    topics: list[str] | None = None,
    require_all_countries: bool = False,
    limit: int = 10,
    page: int = 0,
    timeout: int | None = None,
) -> ResponseSet[ChartResult]:
    """Search for charts matching a query.

    Args:
        query: Search query string.
        countries: Optional list of country names to filter by.
        topics: Optional list of topic names to filter by.
        require_all_countries: If True, only return charts with ALL
            specified countries. Default False (any country matches).
        limit: Maximum results to return (1-100). Default 20.
        page: Page number for pagination (0-indexed). Default 0.
        timeout: HTTP request timeout in seconds. Defaults to client timeout.

    Returns:
        ResponseSet containing ChartResult objects, sorted by popularity (most viewed first).
        Each result includes a `popularity` field (0.0-1.0) based on analytics views.

    Example:
        ```python
        # Basic search (sorted by popularity)
        results = client.charts.search("life expectancy")
        for chart in results:
            print(f"{chart.title}: popularity={chart.popularity:.3f}")

        # Filter by countries
        results = client.charts.search(
            "gdp",
            countries=["France", "Germany"],
            require_all_countries=True
        )

        # Get data from search results
        tb = results[0].fetch()
        ```
    """
    return self._client._site_search.charts(
        query=query,
        countries=countries,
        topics=topics,
        require_all_countries=require_all_countries,
        limit=limit,
        page=page,
        timeout=timeout or self._client.timeout,
    )

fetch

fetch(
    slug_or_url: str,
    *,
    type: ChartType | None = None,
    load_data: bool = True,
    timeout: int | None = None,
) -> ChartTable

Fetch chart data as a ChartTable with rich metadata.

Accepts a chart slug, a slug with query parameters, or a full URL. The slug, query parameters, and chart type are extracted automatically.

Parameters:

  • slug_or_url (str) –

    One of:

    • Chart slug: "life-expectancy"
    • Slug with query params: "education-spending?level=primary&spending_type=gdp_share"
    • Full grapher URL: "https://ourworldindata.org/grapher/life-expectancy?tab=table"
    • Full explorer URL: "https://ourworldindata.org/explorers/covid?Metric=Cases"
  • type (ChartType | None, default: None ) –

    Override the chart type. Defaults to "chart" (grapher). Use "explorerView" for explorer views. Auto-detected from full URLs.

  • load_data (bool, default: True ) –

    If True (default), load full chart data. If False, load only structure (columns and metadata) without rows.

  • timeout (int | None, default: None ) –

    HTTP request timeout in seconds. Defaults to client timeout.

Returns:

  • ChartTable

    ChartTable with chart data and chart_config. Column metadata (unit, description, etc.)

  • ChartTable

    is populated from the chart's metadata.json. Chart config is accessible via .metadata.chart_config.

Note

Explorer views are best-effort. Some explorers may return 503 or other errors from their CSV endpoint.

Example
# Fetch a grapher chart by slug
tb = client.charts.fetch("life-expectancy")

# Fetch with query params (e.g., a multiDim view)
tb = client.charts.fetch("education-spending?level=primary&spending_type=gdp_share")

# Fetch from a full URL (type and query params auto-detected)
tb = client.charts.fetch("https://ourworldindata.org/explorers/covid?Metric=Cases")

# Explicitly fetch an explorer view
tb = client.charts.fetch("covid?Metric=Cases", type="explorerView")
Source code in lib/catalog/owid/catalog/api/charts.py
def fetch(
    self,
    slug_or_url: str,
    *,
    type: ChartType | None = None,
    load_data: bool = True,
    timeout: int | None = None,
) -> ChartTable:
    """Fetch chart data as a ChartTable with rich metadata.

    Accepts a chart slug, a slug with query parameters, or a full URL. The slug,
    query parameters, and chart type are extracted automatically.

    Args:
        slug_or_url: One of:

            - Chart slug: ``"life-expectancy"``
            - Slug with query params: ``"education-spending?level=primary&spending_type=gdp_share"``
            - Full grapher URL: ``"https://ourworldindata.org/grapher/life-expectancy?tab=table"``
            - Full explorer URL: ``"https://ourworldindata.org/explorers/covid?Metric=Cases"``
        type: Override the chart type. Defaults to ``"chart"`` (grapher).
            Use ``"explorerView"`` for explorer views. Auto-detected from full URLs.
        load_data: If True (default), load full chart data.
                   If False, load only structure (columns and metadata) without rows.
        timeout: HTTP request timeout in seconds. Defaults to client timeout.

    Returns:
        ChartTable with chart data and chart_config. Column metadata (unit, description, etc.)
        is populated from the chart's metadata.json. Chart config is accessible via .metadata.chart_config.

    Note:
        Explorer views are best-effort. Some explorers may return 503 or other errors
        from their CSV endpoint.

    Example:
        ```python
        # Fetch a grapher chart by slug
        tb = client.charts.fetch("life-expectancy")

        # Fetch with query params (e.g., a multiDim view)
        tb = client.charts.fetch("education-spending?level=primary&spending_type=gdp_share")

        # Fetch from a full URL (type and query params auto-detected)
        tb = client.charts.fetch("https://ourworldindata.org/explorers/covid?Metric=Cases")

        # Explicitly fetch an explorer view
        tb = client.charts.fetch("covid?Metric=Cases", type="explorerView")
        ```
    """
    effective_timeout = timeout or self._client.timeout
    parsed = parse_chart_slug(slug_or_url)

    slug = parsed.slug
    effective_type = type or parsed.type
    effective_params = parsed.query_params

    # Pick base URL based on type
    if effective_type == EXPLORER:
        base_url = f"{self._site_url}/explorers"
        other_type: ChartType = CHART
    else:
        base_url = self.base_url
        other_type = EXPLORER

    try:
        return _load_chart_table(
            slug,
            load_data=load_data,
            timeout=effective_timeout,
            use_column_short_names=True,
            base_url=base_url,
            extra_params=_parse_query_params(effective_params) if effective_params else None,
        )
    except (requests.HTTPError, ChartNotFoundError) as e:
        hint = "an explorer view" if other_type == EXPLORER else "a grapher chart"
        raise e.__class__(f'{e}\n\nIf this is {hint}, try: fetch("{slug_or_url}", type={other_type!r})') from e

owid.catalog.api.tables.TablesAPI

TablesAPI(client: Client, catalog_url: str)

API for querying and loading tables from the OWID catalog.

Provides methods to search for tables by various criteria and load table data from the catalog.

Example
from owid.catalog import Client

client = Client()

# Search for tables
results = client.tables.search(table="population", namespace="un")

# Load the first result
table = results[0].fetch()

# Fetch table directly by path
tb = client.tables.fetch("garden/un/2024-07-12/un_wpp/population")
print(tb.head())

Initialize the TablesAPI.

Parameters:

  • client (Client) –

    The Client instance.

  • catalog_url (str) –

    Base URL for the catalog (e.g., "https://catalog.ourworldindata.org/").

Methods:

  • search

    Search the catalog for tables matching criteria.

  • fetch

    Fetch a table by catalog path.

Attributes:

Source code in lib/catalog/owid/catalog/api/tables.py
def __init__(self, client: Client, catalog_url: str) -> None:
    """Initialize the TablesAPI.

    Args:
        client: The Client instance.
        catalog_url: Base URL for the catalog (e.g., "https://catalog.ourworldindata.org/").
    """
    self._client = client
    self._catalog_url = catalog_url
    self._index: pd.DataFrame | None = None

catalog_url property

catalog_url: str

Base URL for the catalog (read-only).

search

search(
    table: str | None = None,
    namespace: str | None = None,
    version: str | None = None,
    dataset: str | None = None,
    channel: str | None = None,
    case: bool = False,
    match: Literal[
        "exact", "contains", "regex", "fuzzy"
    ] = "exact",
    fuzzy_threshold: int = 70,
    timeout: int | None = None,
    refresh_index: bool = False,
    latest: bool = False,
) -> ResponseSet[TableResult]

Search the catalog for tables matching criteria.

Parameters:

  • table (str | None, default: None ) –

    Table name pattern to search for

  • namespace (str | None, default: None ) –

    Filter by namespace (exact match)

  • version (str | None, default: None ) –

    Filter by version (exact match)

  • dataset (str | None, default: None ) –

    Dataset name pattern to search for

  • channel (str | None, default: None ) –

    Filter by channel (exact match). Defaults to 'garden' if not specified.

  • case (bool, default: False ) –

    Case-sensitive search (default: False)

  • match (Literal['exact', 'contains', 'regex', 'fuzzy'], default: 'exact' ) –

    How to match table/dataset names (default: "exact"): - "fuzzy": Typo-tolerant similarity matching - "exact": Exact string match - "contains": Substring match - "regex": Regular expression pattern

  • fuzzy_threshold (int, default: 70 ) –

    Minimum similarity score 0-100 for fuzzy matching. Only used when match="fuzzy". (default: 70)

  • timeout (int | None, default: None ) –

    HTTP request timeout in seconds for catalog loading. Defaults to client timeout.

  • refresh_index (bool, default: False ) –

    If True, force re-download of the catalog index. Default False.

  • latest (bool, default: False ) –

    If True, keep only the latest version of each table (grouped by namespace, dataset, table, channel). Default False. Note: results without a version are dropped when this is enabled.

Returns:

  • ResponseSet[TableResult]

    ResponseSet containing matching TableResult objects, sorted by popularity (most viewed first).

  • ResponseSet[TableResult]

    If match="fuzzy", results are sorted by fuzzy relevance score instead.

  • ResponseSet[TableResult]

    Each result includes a popularity field (0.0-1.0) based on analytics views.

Example
# Exact match (default) - searches garden channel by default
results = client.tables.search(table="population")

# Substring match
results = client.tables.search(table="pop", match="contains")

# Regex search
results = client.tables.search(table="population.*density", match="regex")

# Fuzzy search sorted by relevance
results = client.tables.search(table="populaton", match="fuzzy")

# Case-sensitive fuzzy search with custom threshold
results = client.tables.search(table="GDP", match="fuzzy", case=True, fuzzy_threshold=85)

# Filter by namespace and version
results = client.tables.search(
    table="wdi",
    namespace="worldbank_wdi",
    version="2025-09-08",
)

# Search in a specific channel
results = client.tables.search(
    table="wdi",
    namespace="worldbank_wdi",
    version="2025-09-08",
    channel="meadow",
)

# Load a specific result
tb = results[0].fetch()
Source code in lib/catalog/owid/catalog/api/tables.py
def search(
    self,
    table: str | None = None,
    namespace: str | None = None,
    version: str | None = None,
    dataset: str | None = None,
    channel: str | None = None,
    case: bool = False,
    match: Literal["exact", "contains", "regex", "fuzzy"] = "exact",
    fuzzy_threshold: int = 70,
    timeout: int | None = None,
    refresh_index: bool = False,
    latest: bool = False,
) -> ResponseSet[TableResult]:
    """Search the catalog for tables matching criteria.

    Args:
        table: Table name pattern to search for
        namespace: Filter by namespace (exact match)
        version: Filter by version (exact match)
        dataset: Dataset name pattern to search for
        channel: Filter by channel (exact match). Defaults to 'garden' if not specified.
        case: Case-sensitive search (default: False)
        match: How to match table/dataset names (default: "exact"):
            - "fuzzy": Typo-tolerant similarity matching
            - "exact": Exact string match
            - "contains": Substring match
            - "regex": Regular expression pattern
        fuzzy_threshold: Minimum similarity score 0-100 for fuzzy matching.
            Only used when match="fuzzy". (default: 70)
        timeout: HTTP request timeout in seconds for catalog loading. Defaults to client timeout.
        refresh_index: If True, force re-download of the catalog index. Default False.
        latest: If True, keep only the latest version of each table
            (grouped by namespace, dataset, table, channel). Default False.
            Note: results without a version are dropped when this is enabled.

    Returns:
        ResponseSet containing matching TableResult objects, sorted by popularity (most viewed first).
        If match="fuzzy", results are sorted by fuzzy relevance score instead.
        Each result includes a `popularity` field (0.0-1.0) based on analytics views.

    Example:
        ```python
        # Exact match (default) - searches garden channel by default
        results = client.tables.search(table="population")

        # Substring match
        results = client.tables.search(table="pop", match="contains")

        # Regex search
        results = client.tables.search(table="population.*density", match="regex")

        # Fuzzy search sorted by relevance
        results = client.tables.search(table="populaton", match="fuzzy")

        # Case-sensitive fuzzy search with custom threshold
        results = client.tables.search(table="GDP", match="fuzzy", case=True, fuzzy_threshold=85)

        # Filter by namespace and version
        results = client.tables.search(
            table="wdi",
            namespace="worldbank_wdi",
            version="2025-09-08",
        )

        # Search in a specific channel
        results = client.tables.search(
            table="wdi",
            namespace="worldbank_wdi",
            version="2025-09-08",
            channel="meadow",
        )

        # Load a specific result
        tb = results[0].fetch()
        ```
    """
    # Default to garden channel if not specified
    if channel is None:
        channel = "garden"

    # Load and filter catalog index
    index = self._get_index(timeout=timeout, force=refresh_index)
    matches, _ = self._filter_index(
        index,
        table=table,
        dataset=dataset,
        namespace=namespace,
        version=version,
        channel=channel,
        match=match,
        case=case,
        fuzzy_threshold=fuzzy_threshold,
    )

    # Fetch popularity data
    popularity = self._fetch_popularity(matches, timeout)

    # Convert to results
    results = self._to_results(matches, popularity)

    # Keep only latest version per group if requested
    if latest:
        results = _keep_latest_versions(
            results,
            key=lambda r: (r.namespace, r.dataset, r.table, r.channel),
        )

    # Build descriptive query from search parameters
    query = self._build_query(
        table=table,
        namespace=namespace,
        version=version,
        dataset=dataset,
        channel=channel,
    )

    return ResponseSet(
        items=results,
        query=query,
        total_count=len(results),
        base_url=self.catalog_url,
        _ui_advanced=False,
    )

fetch

fetch(
    path: str,
    *,
    load_data: bool = True,
    formats: list[str] | None = None,
    is_public: bool = True,
    timeout: int | None = None,
) -> Table

Fetch a table by catalog path.

Loads the table directly from the catalog.

Parameters:

  • path (str) –

    Full catalog path (e.g., "garden/un/2024-07-12/un_wpp/population").

  • load_data (bool, default: True ) –

    If True (default), load full table data. If False, load only table structure (columns and metadata) without rows.

  • formats (list[str] | None, default: None ) –

    List of formats to try. If None, tries all supported formats.

  • is_public (bool, default: True ) –

    Whether the table is publicly accessible. Default True.

  • timeout (int | None, default: None ) –

    HTTP request timeout in seconds (currently unused, reserved for future).

Returns:

  • Table

    Table with data and metadata (or just metadata if load_data=False).

Raises:

Example
# Load table with data
tb = client.tables.fetch("garden/un/2024-07-12/un_wpp/population")
print(tb.head())

# Load only metadata (no data rows)
tb = client.tables.fetch("garden/un/2024-07-12/un_wpp/population", load_data=False)
print(tb.columns)
Source code in lib/catalog/owid/catalog/api/tables.py
def fetch(
    self,
    path: str,
    *,
    load_data: bool = True,
    formats: list[str] | None = None,
    is_public: bool = True,
    timeout: int | None = None,
) -> Table:
    """Fetch a table by catalog path.

    Loads the table directly from the catalog.

    Args:
        path: Full catalog path (e.g., "garden/un/2024-07-12/un_wpp/population").
        load_data: If True (default), load full table data.
                   If False, load only table structure (columns and metadata) without rows.
        formats: List of formats to try. If None, tries all supported formats.
        is_public: Whether the table is publicly accessible. Default True.
        timeout: HTTP request timeout in seconds (currently unused, reserved for future).

    Returns:
        Table with data and metadata (or just metadata if load_data=False).

    Raises:
        ValueError: If path format is invalid.
        KeyError: If table not found at path.

    Example:
        ```python
        # Load table with data
        tb = client.tables.fetch("garden/un/2024-07-12/un_wpp/population")
        print(tb.head())

        # Load only metadata (no data rows)
        tb = client.tables.fetch("garden/un/2024-07-12/un_wpp/population", load_data=False)
        print(tb.columns)
        ```
    """
    # Validate path format
    catalog_path = CatalogPath.from_str(path)

    if not catalog_path.table:
        raise ValueError(f"Invalid path format: {path}. Expected format: channel/namespace/version/dataset/table")

    return _load_table(
        path, formats=formats, is_public=is_public, load_data=load_data, catalog_url=self.catalog_url
    )

owid.catalog.api.indicators.IndicatorsAPI

IndicatorsAPI(
    client: Client, search_url: str, catalog_url: str
)

API for semantic search of OWID indicators.

Uses the search.owid.io service to find indicators using natural language queries and vector embeddings.

Example
from owid.catalog import Client

client = Client()

# Search for indicators
results = client.indicators.search("solar power generation")
for ind in results:
    print(f"{ind.title} (score: {ind.score:.2f})")

# Fetch the indicator data as a single-column Table
tb = results[0].fetch()

# Or fetch the full table containing the indicator
full_table = results[0].fetch_table()

Initialize the IndicatorsAPI.

Parameters:

  • client (Client) –

    The Client instance.

  • search_url (str) –

    URL for the indicators search API (e.g., "https://search.owid.io/indicators").

  • catalog_url (str) –

    Base URL for the catalog (e.g., "https://catalog.ourworldindata.org/").

Methods:

  • search

    Search for indicators using natural language.

  • fetch

    Fetch a specific indicator by catalog path.

Attributes:

Source code in lib/catalog/owid/catalog/api/indicators.py
def __init__(self, client: Client, search_url: str, catalog_url: str) -> None:
    """Initialize the IndicatorsAPI.

    Args:
        client: The Client instance.
        search_url: URL for the indicators search API (e.g., "https://search.owid.io/indicators").
        catalog_url: Base URL for the catalog (e.g., "https://catalog.ourworldindata.org/").
    """
    self._client = client
    self._search_url = search_url
    self._catalog_url = catalog_url

search_url property

search_url: str

URL for the indicators search API (read-only).

catalog_url property

catalog_url: str

Base URL for the catalog (read-only).

search

search(
    query: str,
    *,
    limit: int = 10,
    show_legacy: bool = False,
    latest: bool = False,
    sort_by: Literal[
        "relevance", "similarity"
    ] = "relevance",
    timeout: int | None = None,
) -> ResponseSet[IndicatorResult]

Search for indicators using natural language.

Uses semantic search to find indicators that match the meaning of your query, not just keyword matching.

Parameters:

  • query (str) –

    Natural language search query (e.g., "renewable energy capacity", "child mortality rate").

  • limit (int, default: 10 ) –

    Maximum number of results to return. Default 10.

  • show_legacy (bool, default: False ) –

    If True, show pre-ETL indicators only. Default False.

  • latest (bool, default: False ) –

    If True, keep only the latest version of each indicator (grouped by namespace, dataset, column_name). Default False. Note: results without a version are dropped when this is enabled.

  • sort_by (Literal['relevance', 'similarity'], default: 'relevance' ) –

    How to sort results (default: "relevance"):

    • "relevance": Combined score blending semantic similarity (60%) and popularity (40%).
    • "similarity": Sort by semantic similarity score only.
  • timeout (int | None, default: None ) –

    HTTP request timeout in seconds. Defaults to client timeout.

Returns:

Example
# Search for indicators (sorted by relevance by default)
results = client.indicators.search("CO2 emissions per capita")

# View results
for ind in results:
    print(f"{ind.title}")
    print(f"  Score: {ind.score:.3f}")
    print(f"  Popularity: {ind.popularity:.3f}")

# Load data from top result
tb = results[0].fetch()

# Sort by semantic similarity only (original behavior)
results = client.indicators.search("CO2 emissions", sort_by="similarity")
Source code in lib/catalog/owid/catalog/api/indicators.py
def search(
    self,
    query: str,
    *,
    limit: int = 10,
    show_legacy: bool = False,
    latest: bool = False,
    sort_by: Literal["relevance", "similarity"] = "relevance",
    timeout: int | None = None,
) -> ResponseSet[IndicatorResult]:
    """Search for indicators using natural language.

    Uses semantic search to find indicators that match the
    meaning of your query, not just keyword matching.

    Args:
        query: Natural language search query
            (e.g., "renewable energy capacity", "child mortality rate").
        limit: Maximum number of results to return. Default 10.
        show_legacy: If True, show pre-ETL indicators only. Default False.
        latest: If True, keep only the latest version of each indicator
            (grouped by namespace, dataset, column_name). Default False.
            Note: results without a version are dropped when this is enabled.
        sort_by: How to sort results (default: "relevance"):

            - "relevance": Combined score blending semantic similarity (60%) and popularity (40%).
            - "similarity": Sort by semantic similarity score only.
        timeout: HTTP request timeout in seconds. Defaults to client timeout.

    Returns:
        SearchResults containing IndicatorResult objects, sorted according to `sort_by`.
        Each result includes a `popularity` field (0.0-1.0) based on analytics views.

    Example:
        ```python
        # Search for indicators (sorted by relevance by default)
        results = client.indicators.search("CO2 emissions per capita")

        # View results
        for ind in results:
            print(f"{ind.title}")
            print(f"  Score: {ind.score:.3f}")
            print(f"  Popularity: {ind.popularity:.3f}")

        # Load data from top result
        tb = results[0].fetch()

        # Sort by semantic similarity only (original behavior)
        results = client.indicators.search("CO2 emissions", sort_by="similarity")
        ```
    """
    params = {
        "query": query,
        "limit": limit,
    }

    resp = session.get(self.search_url, params=params, timeout=timeout or self._client.timeout)

    # Handle HTTP errors with informative messages from response body
    if not resp.ok:
        error_detail = ""
        try:
            error_data = resp.json()
            error_detail = error_data.get("detail", "")
        except Exception:
            pass

        if error_detail:
            raise requests.HTTPError(
                f"{resp.status_code} Error for url: {resp.url} - {error_detail}",
                response=resp,
            )
        else:
            resp.raise_for_status()

    data = resp.json()

    raw_results: list[tuple[dict[str, Any], str | None]] = []
    for r in data.get("results", []):
        path = r.get("catalog_path", "")

        # If legacy indicator, keep only if asked to
        if "/NULL/" in path:
            if not show_legacy:
                # Skip legacy indicators unless requested
                continue
            path = None
        raw_results.append((r, path))

    # Fetch popularity via Datasette API
    slugs = [path for _, path in raw_results if path]
    popularity_data = (
        self._client._datasette.fetch_popularity(
            slugs,
            type="indicator",
            timeout=timeout or self._client.timeout,
        )
        if slugs
        else {}
    )

    results = [
        IndicatorResult(
            indicator_id=r.get("indicator_id", 0),
            title=r.get("title", ""),
            score=r.get("score", 0.0),
            path=path,
            description=r.get("description", ""),
            column_name=r.get("metadata", {}).get("column", ""),
            unit=r.get("metadata", {}).get("unit", ""),
            n_charts=r.get("n_charts", 0),
            popularity=popularity_data.get(path, 0.0) if path else 0.0,
            catalog_url=self.catalog_url,
        )
        for r, path in raw_results
    ]

    # Drop indicators without a version (unless show_legacy=True)
    if not show_legacy:
        results = [r for r in results if r.version is not None]

    # Keep only latest version per group if requested
    if latest:
        results = _keep_latest_versions(
            results,
            key=lambda r: (r.namespace, r.dataset, r.column_name),
        )

    # Sort results according to sort_by parameter
    if sort_by == "similarity":
        # Original behavior: sort by semantic score only
        results.sort(key=lambda r: r.score, reverse=True)
    else:
        # "relevance" (default): blend similarity and popularity
        # Normalize scores to 0-1 range for blending
        max_score = max((r.score for r in results), default=1.0) or 1.0
        results.sort(
            key=lambda r: 0.6 * (r.score / max_score) + 0.4 * r.popularity,
            reverse=True,
        )

    return ResponseSet(
        items=results,
        query=query,
        total_count=data.get("total_results", len(results)),
        base_url=self.catalog_url,
        _ui_advanced=False,
    )

fetch

fetch(
    path: str,
    *,
    load_data: bool = True,
    timeout: int | None = None,
) -> Table

Fetch a specific indicator by catalog path.

Parameters:

  • path (str) –

    Catalog path in format "channel/namespace/version/dataset/table#column"

  • load_data (bool, default: True ) –

    If True (default), load full indicator data. If False, load only structure (columns and metadata) without rows.

  • timeout (int | None, default: None ) –

    HTTP request timeout in seconds (reserved for future use).

Returns:

  • Table

    Table with a single indicator column (plus index). Metadata is preserved.

Raises:

  • ValueError

    If path format is invalid, table not found, or column doesn't exist.

Example
# Fetch indicator by path
tb = client.indicators.fetch("garden/un/2024-07-12/un_wpp/population#population")
print(tb.head())
print(tb["population"].metadata.unit)
Source code in lib/catalog/owid/catalog/api/indicators.py
def fetch(self, path: str, *, load_data: bool = True, timeout: int | None = None) -> Table:
    """Fetch a specific indicator by catalog path.

    Args:
        path: Catalog path in format "channel/namespace/version/dataset/table#column"
        load_data: If True (default), load full indicator data.
                   If False, load only structure (columns and metadata) without rows.
        timeout: HTTP request timeout in seconds (reserved for future use).

    Returns:
        Table with a single indicator column (plus index). Metadata is preserved.

    Raises:
        ValueError: If path format is invalid, table not found, or column doesn't exist.

    Example:
        ```python
        # Fetch indicator by path
        tb = client.indicators.fetch("garden/un/2024-07-12/un_wpp/population#population")
        print(tb.head())
        print(tb["population"].metadata.unit)
        ```
    """
    _ = timeout  # Reserved for future use
    return _load_indicator(path, load_data=load_data, catalog_url=self.catalog_url)

API result types

Result objects returned by fetch() and search() methods.

owid.catalog.api.models.ResponseSet

Bases: BaseModel, Generic[T]

Generic container for API responses.

Provides iteration, indexing, and conversion to CatalogFrame for backwards compatibility.

Attributes:

  • items (list[T]) –

    List of result objects.

  • query (str) –

    The query that produced these results.

  • total_count (int) –

    Total number of results available (may be more than len(items)).

Methods:

  • latest

    Get the most recent result.

  • to_frame

    Convert results to a DataFrame.

  • to_dict

    Convert results to a list of plain dictionaries.

  • filter

    Filter results by predicate function.

  • sort_by

    Sort results by attribute name or key function.

  • set_ui_advanced

    Switch to advanced display showing all fields (type, slug, popularity, etc.).

  • set_ui_basic

    Switch to basic display showing only key fields (title, description, url).

latest
latest(by: str | None = None) -> T

Get the most recent result.

Returns the single item with the highest value for the sort key.

Parameters:

  • by (str | None, default: None ) –

    Attribute name to sort by. If None (default), auto-detects: - ChartResult: uses last_updated (as ISO string with time) - TableResult/IndicatorResult: uses version

Returns:

  • T

    Single item with the highest value for the specified field.

Raises:

  • ValueError

    If no results are available.

  • AttributeError

    If the specified attribute doesn't exist on the results.

Example
>>> # For TableResult/IndicatorResult - auto-detects version
>>> latest_table = results.latest()
>>> tb = latest_table.fetch()

>>> # For ChartResult - auto-detects last_updated
>>> latest_chart = chart_results.latest()
Source code in lib/catalog/owid/catalog/api/models.py
def latest(self, by: str | None = None) -> T:
    """Get the most recent result.

    Returns the single item with the highest value for the sort key.

    Args:
        by: Attribute name to sort by. If None (default), auto-detects:
            - ChartResult: uses last_updated (as ISO string with time)
            - TableResult/IndicatorResult: uses version

    Returns:
        Single item with the highest value for the specified field.

    Raises:
        ValueError: If no results are available.
        AttributeError: If the specified attribute doesn't exist on the results.

    Example:
        ```py
        >>> # For TableResult/IndicatorResult - auto-detects version
        >>> latest_table = results.latest()
        >>> tb = latest_table.fetch()

        >>> # For ChartResult - auto-detects last_updated
        >>> latest_chart = chart_results.latest()
        ```
    """
    if not self.items:
        raise ValueError("No results available to get latest from")

    # Auto-detect sort key based on result type
    if by is None:
        return max(self.items, key=self._get_version_string)

    # Explicit attribute name
    if not hasattr(self.items[0], by):
        # Get available attributes (exclude private ones)
        available = [
            k for k in dir(self.items[0]) if not k.startswith("_") and not callable(getattr(self.items[0], k))
        ]
        raise AttributeError(
            f"Results don't have '{by}' attribute. Available attributes: {', '.join(sorted(available))}"
        )

    return max(self.items, key=lambda item: getattr(item, by))
to_frame
to_frame(all_fields: bool | None = None) -> DataFrame

Convert results to a DataFrame.

Parameters:

  • all_fields (bool | None, default: None ) –

    If True, show all fields. If False, show only key fields. If None (default), use the instance's _ui_advanced setting.

Returns:

  • DataFrame

    DataFrame with one row per result.

Source code in lib/catalog/owid/catalog/api/models.py
def to_frame(self, all_fields: bool | None = None) -> pd.DataFrame:
    """Convert results to a DataFrame.

    Args:
        all_fields: If True, show all fields. If False, show only key fields.
            If None (default), use the instance's _ui_advanced setting.

    Returns:
        DataFrame with one row per result.
    """
    if not self.items:
        return pd.DataFrame()

    # Resolve effective flag: explicit arg > instance setting
    is_advanced = all_fields if all_fields is not None else self._ui_advanced

    # Convert Pydantic models to dicts
    rows = []
    for r in self.items:
        if isinstance(r, BaseModel):
            # For ChartResult, exclude large dict fields for better display
            # Use type name check to avoid circular imports
            if type(r).__name__ == "ChartResult":
                row = {
                    "type": getattr(r, "type", ""),
                    "slug": getattr(r, "slug", ""),
                    "title": getattr(r, "title", ""),
                    "description": getattr(r, "description", ""),
                    "url": getattr(r, "url", ""),
                    "num_related_articles": getattr(r, "num_related_articles", 0),
                    # Only show count of entities, not full list
                    "num_entities": len(getattr(r, "available_entities", [])),
                    "popularity": getattr(r, "popularity", 0),
                    "last_updated": getattr(r, "last_updated", None),
                }

                # Simplify if not advanced UI
                if not is_advanced:
                    row = {
                        "title": row["title"],
                        "description": row["description"],
                        "last_updated": row["last_updated"],
                        "url": row["url"],
                    }
            else:
                row = r.model_dump()

                # Exclude internal config fields that aren't useful to display
                row.pop("catalog_url", None)
                row.pop("base_url", None)

                # Simplify if not advanced UI
                if not is_advanced:
                    row = {
                        "title": row.get("title") or "",
                        "description": row.get("description") or "",
                        "version": row.get("version") or "",
                        "path": row.get("path") or "",
                    }
            rows.append(row)
        else:
            rows.append(r)

    return pd.DataFrame(rows)
to_dict
to_dict() -> list[dict[str, Any]]

Convert results to a list of plain dictionaries.

Useful for serializing results for AI/LLM context windows or any scenario where you need simple dict representations.

Returns:

Example
>>> results = client.charts.search("gdp")
>>> results.to_dict()
[{'slug': 'gdp-per-capita', 'title': 'GDP per capita', ...}, ...]
Source code in lib/catalog/owid/catalog/api/models.py
def to_dict(self) -> list[dict[str, Any]]:
    """Convert results to a list of plain dictionaries.

    Useful for serializing results for AI/LLM context windows
    or any scenario where you need simple dict representations.

    Returns:
        List of dictionaries, one per result item.

    Example:
        ```py
        >>> results = client.charts.search("gdp")
        >>> results.to_dict()
        [{'slug': 'gdp-per-capita', 'title': 'GDP per capita', ...}, ...]
        ```
    """
    if not self.items:
        return []

    if isinstance(self.items[0], BaseModel):
        return [item.model_dump() for item in self.items]  # ty: ignore[unresolved-attribute]

    return list(self.items)  # ty: ignore[invalid-argument-type, invalid-return-type]
filter
filter(predicate: Callable[[T], bool]) -> ResponseSet[T]

Filter results by predicate function.

Returns a new ResponseSet with only items that match the predicate. The predicate should return True for items to keep.

Parameters:

  • predicate (Callable[[T], bool]) –

    Function that takes an item of results (e.g. ChartResult) and returns True/False.

Returns:

  • ResponseSet[T]

    New ResponseSet with filtered results.

Example
>>> # Filter results by version
>>> results.filter(lambda r: r.version > '2024')

>>> # Filter by namespace
>>> results.filter(lambda r: r.namespace == "worldbank")

>>> # Chain multiple filters
>>> results.filter(lambda r: r.version > '2024').filter(lambda r: r.namespace == "un")
Source code in lib/catalog/owid/catalog/api/models.py
def filter(self, predicate: Callable[[T], bool]) -> ResponseSet[T]:
    """Filter results by predicate function.

    Returns a new ResponseSet with only items that match the predicate.
    The predicate should return True for items to keep.

    Args:
        predicate: Function that takes an item of results (e.g. ChartResult) and returns True/False.

    Returns:
        New ResponseSet with filtered results.

    Example:
        ```py
        >>> # Filter results by version
        >>> results.filter(lambda r: r.version > '2024')

        >>> # Filter by namespace
        >>> results.filter(lambda r: r.namespace == "worldbank")

        >>> # Chain multiple filters
        >>> results.filter(lambda r: r.version > '2024').filter(lambda r: r.namespace == "un")
        ```
    """
    filtered_results = [item for item in self.items if predicate(item)]
    return ResponseSet(
        items=filtered_results,
        query=self.query,
        total_count=len(filtered_results),
        base_url=self.base_url,
        _ui_advanced=self._ui_advanced,
    )
sort_by
sort_by(
    key: str | Callable[[T], Any], *, reverse: bool = False
) -> ResponseSet[T]

Sort results by attribute name or key function.

Returns a new ResponseSet with items sorted by the specified key.

Parameters:

  • key (str | Callable[[T], Any]) –

    Either an attribute name (string) or a function that extracts a comparison key from each item.

  • reverse (bool, default: False ) –

    If True, sort in descending order (default: False).

Returns:

  • ResponseSet[T]

    New ResponseSet with sorted results.

Example
>>> # Sort by version (ascending)
>>> results.sort_by('version')

>>> # Sort by version (descending - latest first)
>>> results.sort_by('version', reverse=True)

>>> # Sort by custom function (e.g., by score)
>>> results.sort_by(lambda r: r.score, reverse=True)

>>> # Chain sorting and filtering
>>> results.filter(lambda r: r.version > '2024').sort_by('version', reverse=True)
Source code in lib/catalog/owid/catalog/api/models.py
def sort_by(self, key: str | Callable[[T], Any], *, reverse: bool = False) -> ResponseSet[T]:
    """Sort results by attribute name or key function.

    Returns a new ResponseSet with items sorted by the specified key.

    Args:
        key: Either an attribute name (string) or a function that extracts a comparison key from each item.
        reverse: If True, sort in descending order (default: False).

    Returns:
        New ResponseSet with sorted results.

    Example:
        ```py
        >>> # Sort by version (ascending)
        >>> results.sort_by('version')

        >>> # Sort by version (descending - latest first)
        >>> results.sort_by('version', reverse=True)

        >>> # Sort by custom function (e.g., by score)
        >>> results.sort_by(lambda r: r.score, reverse=True)

        >>> # Chain sorting and filtering
        >>> results.filter(lambda r: r.version > '2024').sort_by('version', reverse=True)
        ```
    """
    if isinstance(key, str):
        # Sort by attribute name
        sorted_results = sorted(self.items, key=lambda item: getattr(item, key), reverse=reverse)
    else:
        # Sort by key function
        sorted_results = sorted(self.items, key=key, reverse=reverse)

    return ResponseSet(
        items=sorted_results,
        query=self.query,
        total_count=self.total_count,
        base_url=self.base_url,
        _ui_advanced=self._ui_advanced,
    )
set_ui_advanced
set_ui_advanced() -> ResponseSet[T]

Switch to advanced display showing all fields (type, slug, popularity, etc.).

Returns:

Example
>>> results.set_ui_advanced()
Source code in lib/catalog/owid/catalog/api/models.py
def set_ui_advanced(self) -> ResponseSet[T]:
    """Switch to advanced display showing all fields (type, slug, popularity, etc.).

    Returns:
        Self (for chaining).

    Example:
        ```py
        >>> results.set_ui_advanced()
        ```
    """
    self._ui_advanced = True
    return self
set_ui_basic
set_ui_basic() -> ResponseSet[T]

Switch to basic display showing only key fields (title, description, url).

Returns:

Example
>>> results.set_ui_basic()
Source code in lib/catalog/owid/catalog/api/models.py
def set_ui_basic(self) -> ResponseSet[T]:
    """Switch to basic display showing only key fields (title, description, url).

    Returns:
        Self (for chaining).

    Example:
        ```py
        >>> results.set_ui_basic()
        ```
    """
    self._ui_advanced = False
    return self

owid.catalog.api.charts.ChartResult

Bases: BaseModel

An OWID chart (from fetch or search).

Fields populated depend on the source: - fetch(): Provides config and metadata - search(): Provides subtitle, available_entities, num_related_articles, published_at, last_updated, popularity

Core fields (slug, title, url) are always populated.

Attributes:

  • slug (str) –

    Chart URL identifier (e.g., "life-expectancy").

  • title (str) –

    Chart title.

  • url (str) –

    Full URL to the interactive chart.

  • config (dict[str, Any]) –

    Raw grapher configuration dict (from fetch).

  • metadata (dict[str, Any]) –

    Chart metadata dict including column info (from fetch).

  • subtitle (str) –

    Chart subtitle/description (from search).

  • available_entities (list[str]) –

    List of entities/countries in the chart (from search).

  • num_related_articles (int) –

    Number of related articles (from search).

  • published_at (datetime | None) –

    When the chart was first published (from search).

  • last_updated (datetime | None) –

    When the chart was last updated (from search).

  • popularity (float) –

    Popularity score (0.0 to 1.0) based on analytics views (from search).

Methods:

  • fetch

    Fetch chart data as ChartTable with rich metadata.

chart_base_url property
chart_base_url: str

Base URL for this chart type (grapher or explorer, derived from site_url and type).

url property
url: str

Full URL to the interactive chart (built from chart_base_url, slug, and query_params).

description property
description: str

Return a string description of the chart result.

fetch
fetch(*, load_data: bool = True) -> ChartTable

Fetch chart data as ChartTable with rich metadata.

Parameters:

  • load_data (bool, default: True ) –

    If True (default), load full chart data. If False, load only structure (columns and metadata) without rows.

Returns:

  • ChartTable

    ChartTable with chart data and chart_config. Column metadata (unit, description, etc.)

  • ChartTable

    is populated from the chart's metadata.json.

Note

Explorer views (type="explorerView") are best-effort. Some explorers may return 503 or other errors from their CSV endpoint. In those cases an :class:ExplorerFetchError is raised with details.

Example
result = client.charts.search("life expectancy")[0]
tb = result.fetch()
print(tb.head())
print(tb["life_expectancy_0"].metadata.unit)
Source code in lib/catalog/owid/catalog/api/charts.py
def fetch(
    self,
    *,
    load_data: bool = True,
) -> ChartTable:
    """Fetch chart data as ChartTable with rich metadata.

    Args:
        load_data: If True (default), load full chart data.
                   If False, load only structure (columns and metadata) without rows.

    Returns:
        ChartTable with chart data and chart_config. Column metadata (unit, description, etc.)
        is populated from the chart's metadata.json.

    Note:
        Explorer views (``type="explorerView"``) are best-effort. Some explorers
        may return 503 or other errors from their CSV endpoint. In those cases an
        :class:`ExplorerFetchError` is raised with details.

    Example:
        ```python
        result = client.charts.search("life expectancy")[0]
        tb = result.fetch()
        print(tb.head())
        print(tb["life_expectancy_0"].metadata.unit)
        ```
    """
    # Return cached if available and requesting full data
    if load_data and self._cached_chart_table is not None:
        return self._cached_chart_table

    try:
        tb = _load_chart_table(
            self.slug,
            load_data=load_data,
            timeout=self._timeout,
            use_column_short_names=True,
            base_url=self.chart_base_url,
            extra_params=_parse_query_params(self.query_params) if self.query_params else None,
        )
    except (requests.HTTPError, ChartNotFoundError) as e:
        if self.type == EXPLORER:
            raise ExplorerFetchError(
                f"Failed to fetch explorer view '{self.slug}'. "
                f"Explorer CSV endpoints are not always available. "
                f"Try fetching the underlying chart directly. Error: {e}"
            ) from e
        raise

    # Cache only if loading full data with default params
    if load_data:
        self._cached_chart_table = tb

    return tb

owid.catalog.api.indicators.IndicatorResult

Bases: BaseModel

An indicator found via semantic search.

Attributes:

  • title (str) –

    Indicator title/name.

  • indicator_id (int | None) –

    Unique indicator ID.

  • path (str | None) –

    Path in the catalog (e.g., "grapher/un/2024-07-12/un_wpp/population#population").

  • channel (str | None) –

    Data channel (parsed from path).

  • namespace (str | None) –

    Data provider namespace (parsed from path).

  • version (str | None) –

    Version string (parsed from path).

  • dataset (str | None) –

    Dataset name (parsed from path).

  • column_name (str) –

    Column name in the table.

  • description (str) –

    Full indicator description.

  • unit (str) –

    Unit of measurement.

  • score (float) –

    Semantic similarity score (0-1).

  • n_charts (int | None) –

    Number of charts using this indicator.

  • popularity (float) –

    Popularity score (0.0 to 1.0) based on analytics views.

Methods:

  • fetch

    Fetch indicator data as a single-column Table.

  • fetch_table

    Fetch the full table containing this indicator.

fetch
fetch(*, load_data: bool = True) -> Table

Fetch indicator data as a single-column Table.

Parameters:

  • load_data (bool, default: True ) –

    If True (default), load full indicator data. If False, load only structure (columns and metadata) without rows.

Returns:

  • Table

    Table with the indicator column (plus index). Metadata is preserved.

Example
result = client.indicators.search("population")[0]
tb = result.fetch()
print(tb.head())
print(tb[tb.columns[0]].metadata.unit)
Source code in lib/catalog/owid/catalog/api/indicators.py
def fetch(self, *, load_data: bool = True) -> Table:
    """Fetch indicator data as a single-column Table.

    Args:
        load_data: If True (default), load full indicator data.
                   If False, load only structure (columns and metadata) without rows.

    Returns:
        Table with the indicator column (plus index). Metadata is preserved.

    Example:
        ```python
        result = client.indicators.search("population")[0]
        tb = result.fetch()
        print(tb.head())
        print(tb[tb.columns[0]].metadata.unit)
        ```
    """
    if self.path is None:
        raise ValueError("Cannot fetch: path is None. Likely a legacy (pre-ETL) indicator.")

    # Return cached if available and requesting full data
    if load_data and self._cached_table is not None:
        return self._cached_table

    tb = _load_indicator(self.path, load_data=load_data, catalog_url=self.catalog_url)

    # Cache only if loading full data
    if load_data:
        self._cached_table = tb

    return tb
fetch_table
fetch_table(*, load_data: bool = True) -> Table

Fetch the full table containing this indicator.

Parameters:

  • load_data (bool, default: True ) –

    If True (default), load full table data. If False, load only structure (columns and metadata) without rows.

Returns:

  • Table

    Table with all columns including this indicator.

Example
result = client.indicators.search("population")[0]
tb = result.fetch_table()
print(tb.columns)
Source code in lib/catalog/owid/catalog/api/indicators.py
def fetch_table(self, *, load_data: bool = True) -> Table:
    """Fetch the full table containing this indicator.

    Args:
        load_data: If True (default), load full table data.
                   If False, load only structure (columns and metadata) without rows.

    Returns:
        Table with all columns including this indicator.

    Example:
        ```python
        result = client.indicators.search("population")[0]
        tb = result.fetch_table()
        print(tb.columns)
        ```
    """
    # Return cached if available and requesting full data
    if load_data and self._cached_table is not None:
        return self._cached_table

    result = self._load_full_table(load_data=load_data)

    # Cache only if loading full data
    if load_data:
        self._cached_table = result

    return result

owid.catalog.api.tables.TableResult

Bases: BaseModel

A table found in the catalog.

Attributes:

  • table (str) –

    Table name.

  • path (str) –

    Full path to the table.

  • channel (str) –

    Data channel (garden, meadow, etc.).

  • namespace (str) –

    Data provider namespace.

  • version (str) –

    Version string.

  • dataset (str) –

    Dataset name.

  • dimensions (list[str]) –

    List of dimension columns.

  • title (str | None) –

    Human-readable title (from table or dataset metadata).

  • description (str | None) –

    Detailed description (from table or dataset metadata).

  • is_public (bool) –

    Whether the data is publicly accessible.

  • formats (list[str]) –

    List of available formats.

  • popularity (float) –

    Popularity score (0.0 to 1.0) based on analytics views.

Methods:

  • fetch

    Fetch table data.

fetch
fetch(*, load_data: bool = True) -> Table

Fetch table data.

Parameters:

  • load_data (bool, default: True ) –

    If True (default), load full table data. If False, load only structure (columns and metadata) without rows.

Returns:

  • Table

    Table with data and metadata (or just metadata if load_data=False).

Example
result = client.tables.search(table="population")[0]
tb = result.fetch()
print(tb.head())
print(tb.columns)
Source code in lib/catalog/owid/catalog/api/tables.py
def fetch(self, *, load_data: bool = True) -> Table:
    """Fetch table data.

    Args:
        load_data: If True (default), load full table data.
                   If False, load only structure (columns and metadata) without rows.

    Returns:
        Table with data and metadata (or just metadata if load_data=False).

    Example:
        ```python
        result = client.tables.search(table="population")[0]
        tb = result.fetch()
        print(tb.head())
        print(tb.columns)
        ```
    """
    # Return cached if available and requesting full data
    if load_data and self._cached_table is not None:
        return self._cached_table

    tb = _load_table(
        self.path,
        formats=self.formats,
        is_public=self.is_public,
        load_data=load_data,
        catalog_url=self.catalog_url,
    )

    # Cache only if loading full data
    if load_data:
        self._cached_table = tb

    return tb