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, day

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Return datetime.timedelta from utcoffset() and dst() (e.g. timedelta(hours=5)) or None for unknown
  2. Wrap numeric offsets: timedelta(seconds=offset) instead of a bare int
  3. 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

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


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