pola-rs/polars · error

unexpected time zone offset: {offset!r}

Error message

unexpected time zone offset: {offset!r}

What it means

_parse_fixed_tz_offset (lru_cached) parses fixed UTC offset strings by appending them to an ISO datetime and calling datetime.fromisoformat. If the offset is not in the strict ±HH:MM form that fromisoformat accepts (e.g. "UTC+02:00", "+0200", "+2", or trailing garbage), the ValueError from fromisoformat is re-raised as this error with the offending offset.

Source

Thrown at py-polars/src/polars/_utils/convert.py:192

        tz = _parse_fixed_tz_offset(time_zone)

    return dt.astimezone(tz)


# cache here as we have a single tz per column
# and this function will be called on every conversion
@lru_cache(16)
def _parse_fixed_tz_offset(offset: str) -> tzinfo:
    try:
        # use fromisoformat to parse the offset
        dt_offset = datetime.fromisoformat("2000-01-01T00:00:00" + offset)

        # alternatively, we parse the offset ourselves extracting hours and
        # minutes, then we can construct:
        # tzinfo=timezone(timedelta(hours=..., minutes=...))
    except ValueError:
        msg = f"unexpected time zone offset: {offset!r}"
        raise ValueError(msg) from None

    return dt_offset.tzinfo  # type: ignore[return-value]


def to_py_timedelta(value: int | float, time_unit: TimeUnit) -> timedelta:
    """Convert an integer or float to a Python timedelta object."""
    if time_unit == "us":
        return timedelta(microseconds=value)
    elif time_unit == "ns":
        return timedelta(microseconds=value // 1_000)
    elif time_unit == "ms":
        return timedelta(milliseconds=value)
    else:
        _raise_invalid_time_unit(time_unit)


def to_py_decimal(prec: int, value: str) -> Decimal:
    """Convert decimal components to a Python Decimal object."""

View on GitHub (pinned to df599052da)

Solutions

  1. Normalize the offset to ±HH:MM before parsing (strip 'UTC'/'GMT', insert the colon).
  2. Upgrade to Python ≥ 3.11, where fromisoformat accepts "+0200" and more variants.
  3. Convert the offset yourself: timezone(timedelta(hours=h, minutes=m)) and pass tzinfo directly instead of the string.

Example fix

// before
pl.Series(["2024-01-01T00:00:00+0200"]).str.to_datetime()

// after
import re
s = pl.Series(["2024-01-01T00:00:00+0200"])
fixed = re.sub(r"([+-]\d{2})(\d{2})$", r"\1:\2", s[0])
pl.Series([fixed]).str.to_datetime()
Defensive patterns

Strategy: validation

Validate before calling

import re

VALID_FIXED_OFFSET = re.compile(r"^[+-]\d{2}:\d{2}(:\d{2}(\.\d+)?)?$")

def normalize_offset(off: str) -> str:
    off = re.sub(r"^(?:UTC|GMT)", "", off.strip())
    m = re.fullmatch(r"([+-]\d{2})(\d{2})", off)
    if m:
        off = f"{m.group(1)}:{m.group(2)}"
    if not VALID_FIXED_OFFSET.fullmatch(off):
        raise ValueError(f"unexpected time zone offset: {off!r}")
    return off

Type guard

def is_iso_fixed_offset(off: str) -> bool:
    import re
    return bool(re.fullmatch(r"[+-]\d{2}:\d{2}(:\d{2}(\.\d+)?)?", off))

Try / catch

try:
    s = pl.Series([dt_string]).str.to_datetime()
except ValueError as e:
    if "unexpected time zone offset" in str(e):
        s = pl.Series([normalize_offset_string(dt_string)]).str.to_datetime()
    else:
        raise

Prevention

When it happens

Trigger: Datetime strings with non-canonical fixed offsets ("2024-01-01T00:00:00UTC+02:00", "+0200" on Python < 3.11); offsets like "+0530" without a colon on older interpreters; stray characters appended to the offset field.

Common situations: Parsing logs or exports whose tz field uses "GMT+2"-style or colon-less formats; running on Python 3.10 or older where fromisoformat only accepts ±HH:MM[:SS[.ffffff]]; pandas→polars conversion of tz-aware data with unusual fixed offsets.

Related errors


AI-assisted analysis of pola-rs/polars@df599052da (2026-08-16). Data as JSON: /api/errors/cc0b883f7220fa0c. Report an issue: GitHub.