Skip to content

Error Handling

pyBDL raises structured exceptions for all error conditions. All exceptions inherit from BDLError so you can catch the entire hierarchy with a single except clause.

Exception hierarchy

BDLError (base)
├── BDLHTTPError               - HTTP-level error (4xx, 5xx, network failure)
├── BDLResponseError           - Unexpected or invalid API payload
└── BDLRateLimitError          - Client-side quota exceeded
    └── BDLRateLimitDelayError - Wait time would exceed max_delay

BDLHTTPError

Raised when the BDL server responds with an HTTP error or when the request fails entirely (for example DNS failure or timeout).

from pybdl.api.exceptions import BDLHTTPError

try:
    data = bdl.data.get_data_by_variable("invalid-id", years=[2021])
except BDLHTTPError as e:
    print(f"HTTP {e.status_code} from {e.url}")
    print(f"Body: {e.response_body}")

Useful attributes:

  • e.status_code - HTTP status code (int | None)
  • e.url - request URL (str | None)
  • e.response_body - raw response body

BDLResponseError

Raised when the API returns a response that pyBDL cannot parse (unexpected structure, missing required fields).

from pybdl.api.exceptions import BDLResponseError

try:
    data = bdl.data.get_data_by_variable("3643", years=[2021])
except BDLResponseError as e:
    print(f"Unexpected payload: {e.payload}")

BDLRateLimitError

Raised when the client-side quota is exhausted and raise_on_rate_limit=True is configured. The default behavior is to wait, not raise.

from pybdl import BDL, BDLConfig
from pybdl.api.exceptions import BDLRateLimitDelayError, BDLRateLimitError

config = BDLConfig(api_key="...", raise_on_rate_limit=True)
bdl = BDL(config)

try:
    data = bdl.data.get_data_by_variable("3643", years=[2021])
except BDLRateLimitDelayError as e:
    print(f"Would need to wait {e.actual_delay:.1f}s (max allowed: {e.max_delay:.1f}s)")
except BDLRateLimitError as e:
    print(f"Quota exceeded - retry in {e.retry_after:.1f}s")
    print(f"Quota details: {e.limit_info}")

BDLRateLimitDelayError is a subclass of BDLRateLimitError, so catch it first.

Catching all pyBDL errors

from pybdl.api.exceptions import BDLError

try:
    data = bdl.data.get_data_by_variable("3643", years=[2021])
except BDLError as e:
    print(f"pyBDL error: {e}")

HTTP 429 from the server

When the server responds with HTTP 429 (Too Many Requests), pyBDL retries automatically using the http_429_max_retries / BDL_HTTP_429_MAX_RETRIES budget. Waits follow client-side quota when exhausted; otherwise exponential backoff applies (see Rate limiting). This is separate from raise_on_rate_limit and from request_retries (which covers 5xx errors).

The server may return undocumented X-Rate-Limit-* headers; pyBDL does not interpret them. See Rate limiting — server headers.

BDLQuotaDesyncWarning

If the server returns HTTP 429 while the client-side rate limiter still has an immediate slot, pyBDL emits BDLQuotaDesyncWarning (a UserWarning subclass) and retries with exponential backoff. Enable shared quota_cache or reduce parallelism if this warning appears often.

import warnings
from pybdl.api.exceptions import BDLQuotaDesyncWarning

warnings.filterwarnings("once", category=BDLQuotaDesyncWarning)

API reference

exceptions

Exceptions for pyBDL API client.

BDLError

Bases: Exception

Base exception for all pyBDL errors.

BDLHTTPError

BDLHTTPError(
    *,
    status_code,
    response_body=None,
    url=None,
    message=None,
)

Bases: BDLError

Raised when the BDL API responds with an HTTP error or the request fails.

Source code in pybdl/api/exceptions.py
def __init__(
    self,
    *,
    status_code: int | None,
    response_body: Any = None,
    url: str | None = None,
    message: str | None = None,
) -> None:
    self.status_code = status_code
    self.response_body = response_body
    self.url = url

    if message is None:
        parts = ["BDL request failed"]
        if status_code is not None:
            parts.append(f"with HTTP {status_code}")
        if url:
            parts.append(f"for {url}")
        if response_body not in (None, ""):
            parts.append(f": {response_body}")
        message = " ".join(parts)
    super().__init__(message)

status_code instance-attribute

status_code = status_code

response_body instance-attribute

response_body = response_body

url instance-attribute

url = url

BDLResponseError

BDLResponseError(message, *, payload=None)

Bases: BDLError

Raised when the BDL API returns an unexpected or invalid payload.

Source code in pybdl/api/exceptions.py
def __init__(self, message: str, *, payload: Any = None) -> None:
    self.payload = payload
    super().__init__(message)

payload instance-attribute

payload = payload

BDLRateLimitError

BDLRateLimitError(
    retry_after, limit_info=None, message=None
)

Bases: BDLError

Raised when rate limit is exceeded.

Source code in pybdl/api/exceptions.py
def __init__(
    self,
    retry_after: float,
    limit_info: dict[str, Any] | None = None,
    message: str | None = None,
) -> None:
    self.retry_after = retry_after
    self.limit_info = limit_info or {}

    if message is None:
        periods = ", ".join(f"{info['limit']} req/{info['period']}s" for info in self.limit_info.get("quotas", []))
        message = f"Rate limit exceeded ({periods}). Retry after {retry_after:.1f}s."

    super().__init__(message)

retry_after instance-attribute

retry_after = retry_after

limit_info instance-attribute

limit_info = limit_info or {}

BDLQuotaDesyncWarning

Bases: UserWarning

Server returned HTTP 429 while client-side quota reports an immediate slot.

BDLRateLimitDelayError

BDLRateLimitDelayError(
    actual_delay, max_delay, limit_info=None
)

Bases: BDLRateLimitError

Raised when required delay exceeds max_delay setting.

Source code in pybdl/api/exceptions.py
def __init__(
    self,
    actual_delay: float,
    max_delay: float,
    limit_info: dict[str, Any] | None = None,
) -> None:
    self.actual_delay = actual_delay
    self.max_delay = max_delay

    message = f"Required delay ({actual_delay:.1f}s) exceeds maximum allowed delay ({max_delay:.1f}s)."
    super().__init__(
        retry_after=actual_delay,
        limit_info=limit_info,
        message=message,
    )

actual_delay instance-attribute

actual_delay = actual_delay

max_delay instance-attribute

max_delay = max_delay

retry_after instance-attribute

retry_after = retry_after

limit_info instance-attribute

limit_info = limit_info or {}