python/cpython · error · TypeError

tzinfo. () must return None or timedelta, not

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()).

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.

Appendix: 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)