python/cpython · error · TypeError
tzinfo.{name}() must return None or timedelta, not {type(off
Error message
tzinfo.{name}() must return None or timedelta, not {type(offset).__name__!r} What it means
Raised by _check_utc_offset() when a tzinfo subclass's utcoffset(dt) or dst(dt) method returns something that is neither None nor a datetime.timedelta. The message names which method misbehaved. The datetime module validates these return values whenever it needs an offset (aware arithmetic, comparisons, strftime, timestamp()).
Source
Thrown at Lib/_pydatetime.py:561
# Just raise TypeError if the arg isn't None or a string.
def _check_tzname(name):
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, dayView on GitHub (pinned to bc6749cc3b)
Solutions
- Return datetime.timedelta from utcoffset() and dst() (e.g. timedelta(hours=5)) or None for unknown
- Wrap numeric offsets: timedelta(seconds=offset) instead of a bare int
- In tests, configure mocks with return_value=timedelta(0), not integers
Example fix
// before
class MyTZ(tzinfo):
def utcoffset(self, dt):
return 3600 # seconds -> TypeError
// after
class MyTZ(tzinfo):
def utcoffset(self, dt):
return timedelta(hours=1) Defensive patterns
Strategy: type-guard
Validate before calling
off = tz.utcoffset(dt) assert off is None or isinstance(off, timedelta), 'utcoffset() must return None or timedelta'
Type guard
from datetime import timedelta
def valid_offset(v) -> bool:
return v is None or isinstance(v, timedelta) Prevention
- Wrap numeric offsets in timedelta at the source, never return bare ints from utcoffset/dst
- Configure test mocks with return_value=timedelta(hours=N)
When it happens
Trigger: A custom tzinfo whose utcoffset() returns an int like 3600 (seconds) instead of timedelta(hours=1); a dst() that returns 0 or a string; called via dt.astimezone(), dt.utcoffset(), aware comparisons, or isoformat() on an aware datetime.
Common situations: Developers assuming seconds-based offsets from other languages/libraries; tzinfo shims around pytz or dateutil returning raw numbers; test doubles returning MagicMock instead of timedelta.
Related errors
- tzinfo.tzname() must return None or string, not {type(name).
- tzinfo argument must be None or of a tzinfo subclass, not {t
- offset must be a timedelta strictly between -timedelta(hours
- fromutc() requires a datetime argument
- Malformed time zone string
AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14).
Data as JSON: /api/errors/f0b3e01544be0d64.
Report an issue: GitHub.