python/cpython · error · ValueError
offset must be a timedelta strictly between -timedelta(hours
Error message
offset must be a timedelta strictly between -timedelta(hours=24) and timedelta(hours=24), not {offset!r} What it means
Raised by _check_utc_offset() when utcoffset() or dst() returns a timedelta whose absolute value is not strictly less than one day (timedelta(1)). UTC offsets physically cannot reach ±24 hours, so datetime enforces -timedelta(hours=24) < offset < timedelta(hours=24). Note the strictness: exactly ±24h is also rejected.
Source
Thrown at Lib/_pydatetime.py:564
if name is not None and not isinstance(name, str):
raise TypeError("tzinfo.tzname() must return None or string, "
f"not {type(name).__name__!r}")
# name is the offset-producing method, "utcoffset" or "dst".
# offset is what it returned.
# If offset isn't None or timedelta, raises TypeError.
# If offset is None, returns None.
# Else offset is checked for being in range.
# If it is, its integer value is returned. Else ValueError is raised.
def _check_utc_offset(name, offset):
assert name in ("utcoffset", "dst")
if offset is None:
return
if not isinstance(offset, timedelta):
raise TypeError(f"tzinfo.{name}() must return None "
f"or timedelta, not {type(offset).__name__!r}")
if not -timedelta(1) < offset < timedelta(1):
raise ValueError("offset must be a timedelta "
"strictly between -timedelta(hours=24) and "
f"timedelta(hours=24), not {offset!r}")
def _check_date_fields(year, month, day):
year = _index(year)
month = _index(month)
day = _index(day)
if not MINYEAR <= year <= MAXYEAR:
raise ValueError(f"year must be in {MINYEAR}..{MAXYEAR}, not {year}")
if not 1 <= month <= 12:
raise ValueError(f"month must be in 1..12, not {month}")
dim = _days_in_month(year, month)
if not 1 <= day <= dim:
raise ValueError(f"day {day} must be in range 1..{dim} for month {month} in year {year}")
return year, month, day
def _check_time_fields(hour, minute, second, microsecond, fold):
hour = _index(hour)View on GitHub (pinned to bc6749cc3b)
Solutions
- Fix the offset computation so |offset| < timedelta(hours=24)
- Check unit conversions (days*24 vs hours) in the tzinfo implementation
- Clamp pathological offsets in the tzinfo hook before returning
Example fix
// before
def utcoffset(self, dt):
return timedelta(days=self.offset_hours) # wrong unit
// after
def utcoffset(self, dt):
return timedelta(hours=self.offset_hours) Defensive patterns
Strategy: validation
Validate before calling
from datetime import timedelta
def safe_offset(off):
if off is None:
return None
if not -timedelta(hours=24) < off < timedelta(hours=24):
raise ValueError(f'offset {off} out of range')
return off Try / catch
try:
dt.astimezone(tz)
except ValueError as e:
if 'offset must be a timedelta' in str(e):
raise # buggy tzinfo implementation, fix it
raise Prevention
- Double-check day-vs-hour units in offset math
- Test custom tzinfo at boundary values like timedelta(hours=23, minutes=59)
When it happens
Trigger: A custom tzinfo returning timedelta(hours=24) or timedelta(days=2) from utcoffset(); a bug computing offset as total days instead of hours; offsets accumulated across DST plus base offset exceeding 24h.
Common situations: Unit-conversion mistakes (days vs hours) in hand-rolled zoneinfo replacements; clamping or sign errors producing -timedelta(1); historical LMT offsets near ±24h.
Related errors
- Malformed time zone string
- tzinfo.tzname() must return None or string, not {type(name).
- tzinfo.{name}() must return None or timedelta, not {type(off
- tzinfo argument must be None or of a tzinfo subclass, not {t
- dt.tzinfo is not self
AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14).
Data as JSON: /api/errors/10609ad52498a4f1.
Report an issue: GitHub.