python/cpython · error · ValueError

Malformed time zone string

Error message

Malformed time zone string

What it means

After a timezone sign ('+'/'-') or 'Z' is found, the remaining tz string must match one of the documented lengths: 2 (HH), 4 (HHMM), 5 (HH:MM), 6 (HHMMSS), 7+ (HHMMSS.f), 8 (HH:MM:SS), 10+ (HH:MM:SS.f). Lengths 0, 1, 3 — or a 'Z' followed by more characters — raise ValueError('Malformed time zone string').

Source

Thrown at Lib/_pydatetime.py:485

            error_from_components = True

    tzi = None
    if tz_pos == len_str and tstr[-1] == 'Z':
        tzi = timezone.utc
    elif tz_pos > 0:
        tzstr = tstr[tz_pos:]

        # Valid time zone strings are:
        # HH                  len: 2
        # HHMM                len: 4
        # HH:MM               len: 5
        # HHMMSS              len: 6
        # HHMMSS.f+           len: 7+
        # HH:MM:SS            len: 8
        # HH:MM:SS.f+         len: 10+

        if len(tzstr) in (0, 1, 3) or tstr[tz_pos-1] == 'Z':
            raise ValueError("Malformed time zone string")

        tz_comps = _parse_hh_mm_ss_ff(tzstr)

        if all(x == 0 for x in tz_comps):
            tzi = timezone.utc
        else:
            tzsign = -1 if tstr[tz_pos - 1] == '-' else 1

            try:
                # This function is intended to validate datetimes, but because
                # we restrict time zones to ±24h, it serves here as well.
                _check_time_fields(hour=tz_comps[0], minute=tz_comps[1],
                                   second=tz_comps[2], microsecond=tz_comps[3],
                                   fold=0)
            except ValueError as e:
                error_from_tz = e
            else:
                td = timedelta(hours=tz_comps[0], minutes=tz_comps[1],

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Emit offsets with strftime('%z') or isoformat(), producing '+HHMM'/'±HH:MM[:SS[.f]]'
  2. Zero-pad when building manually: f'{sign}{hh:02d}:{mm:02d}'
  3. Treat a trailing 'Z' as terminal: strip everything after it or reject 'Z+offset' combinations at validation

Example fix

// before
tz = f'+{hours}:{minutes}'          # '+1:30' shape issues / '+1' length errors
dt = datetime.fromisoformat(f'{s}{tz}')

# after
tz = f'+{hours:02d}:{minutes:02d}'  # '+01:30'
dt = datetime.fromisoformat(f'{s}{tz}')
Defensive patterns

Strategy: validation

Validate before calling

def build_offset(sign: int, hh: int, mm: int, ss: int = 0) -> str:
    return f"{'+' if sign >= 0 else '-'}{hh:02d}:{mm:02d}" + (f':{ss:02d}' if ss else '')

Type guard

def offset_shape_ok(tz: str) -> bool:
    import re
    return re.fullmatch(r'(Z|[+-]\d{2}(:?\d{2}(:?\d{2}([.,]\d+)?)?)?)', tz) is not None

Prevention

When it happens

Trigger: datetime.fromisoformat('2021-01-01T12:30:00+0'); '+01:0' (length 5 is fine but '+01' length 3 is not); '...Z01:00' (Z plus trailing offset); '...+01:0' vs '+010' (length 4 needs compact HHMM).

Common situations: Offsets hand-built as f'+{tz}' without zero-padding hours/minutes; slicing offsets to fixed width and cutting a digit; supporting 'Z' plus offset simultaneously in serialized data; JSON payloads where the offset lost a leading zero ('+1:00' length 5 parses as malformed content elsewhere, '+1' length 2 with bad digits errors later).

Understand the failure class

Related errors


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/607fbb6549f65633. Report an issue: GitHub.