pola-rs/polars · error

unexpected time zone offset

Error message

unexpected time zone offset: {offset!r}

What it means

`_parse_fixed_tz_offset` parses fixed UTC offset strings like '+05:30' when localizing datetimes. If the offset string cannot be parsed into hours/minutes (malformed format), ValueError 'unexpected time zone offset' is raised with the original offset value.

Solutions

  1. Use a full ISO 8601 offset format: '+05:30' or '-08:00'.
  2. Use zoneinfo.ZoneInfo('Asia/Kolkata') instead of fixed-offset strings.
  3. Normalize the offset string before passing: pad to +HH:MM with two-digit hours and minutes.

Example fix

// before
ts = pl.Series(['2024-01-01T00:00+5']).str.to_datetime()
// after
ts = pl.Series(['2024-01-01T00:00+05:00']).str.to_datetime()
Defensive patterns

Strategy: validation

Validate before calling

import re
if not re.fullmatch(r'[+-]\d{2}:?\d{2}', offset):
    raise ValueError(f'offset {offset!r} must look like +HH:MM')

Try / catch

try:
    result = convert_fn(dt_with_offset)
except ValueError as e:
    if 'unexpected time zone offset' in str(e):
        dt = dt.replace(tzinfo=datetime.timezone(datetime.timedelta(hours=5, minutes=30)))
        result = convert_fn(dt)
    else:
        raise

Prevention

When it happens

Trigger: Converting a Python datetime whose tzinfo renders as a fixed-offset string that fails parsing, e.g. pl.from_datetime / to_local_datetime paths via _localize_datetime with offsets like '+5' (no minutes), '+0530' variants the parser doesn't accept, or garbage offsets.

Common situations: Hand-built tzinfo strings from config; offsets produced by other libraries in non-ISO formats; timezone strings stored in data files with inconsistent formatting.

Related errors


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

Appendix: 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 fe841f959e)