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
- Emit offsets with strftime('%z') or isoformat(), producing '+HHMM'/'±HH:MM[:SS[.f]]'
- Zero-pad when building manually: f'{sign}{hh:02d}:{mm:02d}'
- 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
- Always produce offsets with strftime('%z') or isoformat()
- Zero-pad offset hours and minutes
- Never append characters after a 'Z' marker
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Invalid ISO string
- Invalid isoformat string
- Inconsistent use of dash separator
- Incomplete time component
- Invalid time separator: %c
AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14).
Data as JSON: /api/errors/607fbb6549f65633.
Report an issue: GitHub.