python/cpython · error · ValueError

fromutc(): dt.dst gave inconsistent results; cannot convert

Error message

fromutc(): dt.dst gave inconsistent results; cannot convert

What it means

Raised by tzinfo.fromutc() when dst() returns a value before the adjustment but None after the datetime was shifted by delta = utcoffset - dst. The default algorithm assumes dst() is consistent across that small time shift; getting a timedelta first and then None means the timezone implementation contradicts itself, so conversion is abandoned with this ValueError.

Source

Thrown at Lib/_pydatetime.py:1354

        if dt.tzinfo is not self:
            raise ValueError("dt.tzinfo is not self")

        dtoff = dt.utcoffset()
        if dtoff is None:
            raise ValueError("fromutc() requires a non-None utcoffset() "
                             "result")

        # See the long comment block at the end of this file for an
        # explanation of this algorithm.
        dtdst = dt.dst()
        if dtdst is None:
            raise ValueError("fromutc() requires a non-None dst() result")
        delta = dtoff - dtdst
        if delta:
            dt += delta
            dtdst = dt.dst()
            if dtdst is None:
                raise ValueError("fromutc(): dt.dst gave inconsistent "
                                 "results; cannot convert")
        return dt + dtdst

    # Pickle support.

    def __reduce__(self):
        getinitargs = getattr(self, "__getinitargs__", None)
        if getinitargs:
            args = getinitargs()
        else:
            args = ()
        return (self.__class__, args, self.__getstate__())


class IsoCalendarDate(tuple):

    def __new__(cls, year, week, weekday, /):
        return super().__new__(cls, (year, week, weekday))

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Make dst() deterministic and total: return a timedelta (possibly timedelta(0)) for every datetime, never None inside fromutc's operating range
  2. Override fromutc() in the subclass with transition-aware logic instead of relying on the base algorithm
  3. Adopt zoneinfo.ZoneInfo, which has correct, consistent transition handling

Example fix

// before
class BadTZ(tzinfo):
    def utcoffset(self, dt): return timedelta(hours=1)
    def dst(self, dt):
        return None if (dt and dt.hour < 3) else timedelta(hours=1)
    def tzname(self, dt): return 'B'

// after
class BadTZ(tzinfo):
    def utcoffset(self, dt): return timedelta(hours=1)
    def dst(self, dt): return timedelta(hours=1)
    def tzname(self, dt): return 'B'
Defensive patterns

Strategy: validation

Validate before calling

# verify dst() is stable under the offset shift fromutc performs
d1 = dt.replace(tzinfo=tz).dst()
if d1 is not None:
    d2 = (dt - (dt.replace(tzinfo=tz).utcoffset() - d1)).replace(tzinfo=tz).dst()
    if d2 is None:
        raise ValueError('dst() inconsistent under offset shift')

Try / catch

try:
    local = tz.fromutc(dt)
except ValueError as e:
    if 'inconsistent' in str(e):
        raise RuntimeError(f'broken tzinfo implementation: {type(tz).__name__}') from e
    raise

Prevention

When it happens

Trigger: A custom dst(self, dt) that returns timedelta(hours=1) for some clock times and None for others, where the shifted time lands in the None branch; dst() keyed on wall-clock hour with gaps; zone implementations whose DST predicate changes discontinuously within the offset window.

Common situations: Hand-written DST rules that use wall-clock comparisons without normalization; timezone mocks in tests that vary by call count or hour; buggy ports of IANA transition tables.

Related errors


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