python/cpython · error · ValueError

year must be in {MINYEAR}..{MAXYEAR}, not {year}

Error message

year must be in {MINYEAR}..{MAXYEAR}, not {year}

What it means

_isoweek_to_gregorian (backing date.fromisocalendar and the ISO-week parse path of fromisoformat) requires the ISO year within datetime.MINYEAR..MAXYEAR (1..9999) because week 1 of an ISO year can resolve into the previous Gregorian year. Out-of-range years raise ValueError with the allowed range echoed.

Source

Thrown at Lib/_pydatetime.py:515

                _check_time_fields(hour=tz_comps[0], minute=tz_comps[1],
                                   second=tz_comps[2], microsecond=tz_comps[3],
                                   fold=0)
            except ValueError as e:
                error_from_tz = e
            else:
                td = timedelta(hours=tz_comps[0], minutes=tz_comps[1],
                               seconds=tz_comps[2], microseconds=tz_comps[3])
                tzi = timezone(tzsign * td)

    time_comps.append(tzi)

    return time_comps, became_next_day, error_from_components, error_from_tz

# tuple[int, int, int] -> tuple[int, int, int] version of date.fromisocalendar
def _isoweek_to_gregorian(year, week, day):
    # Year is bounded this way because 9999-12-31 is (9999, 52, 5)
    if not MINYEAR <= year <= MAXYEAR:
        raise ValueError(f"year must be in {MINYEAR}..{MAXYEAR}, not {year}")

    if not 0 < week < 53:
        out_of_range = True

        if week == 53:
            # ISO years have 53 weeks in them on years starting with a
            # Thursday and leap years starting on a Wednesday
            first_weekday = _ymd2ord(year, 1, 1) % 7
            if (first_weekday == 4 or (first_weekday == 3 and
                                       _is_leap(year))):
                out_of_range = False

        if out_of_range:
            raise ValueError(f"Invalid week: {week}")

    if not 0 < day < 8:
        raise ValueError(f"Invalid weekday: {day} (range is [1, 7])")

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Clamp or reject years outside 1..9999 before calling fromisocalendar
  2. Fix upstream year expansion (century windowing) so two-digit years map into range
  3. Use try/except ValueError around calendar conversion at the trust boundary and report the bad row/record

Example fix

// before
d = date.fromisocalendar(year, week, day)  # year 0 -> ValueError

# after
if not 1 <= year <= 9999:
    raise ValueError(f'bad ISO year {year!r} in record')
d = date.fromisocalendar(year, week, day)
Defensive patterns

Strategy: validation

Validate before calling

from datetime import MINYEAR, MAXYEAR
def check_iso_year(year: int) -> None:
    if not MINYEAR <= year <= MAXYEAR:
        raise ValueError(f'ISO year {year} outside {MINYEAR}..{MAXYEAR}')

Type guard

def is_valid_iso_year(y: object) -> bool:
    return isinstance(y, int) and 1 <= y <= 9999

Try / catch

try:
    d = date.fromisocalendar(y, w, wd)
except ValueError as e:
    raise ValueError(f'invalid calendar input {y}/{w}/{wd}: {e}') from e

Prevention

When it happens

Trigger: date.fromisocalendar(0, 1, 1) or (10000, 1, 1); datetime.fromisoformat('10000-W01-1') reaching the week path; user-supplied year 0 from a spreadsheet or form used directly as the ISO year.

Common situations: Excel/CSV artifacts exporting year 0 or 1899; two-digit years expanded wrongly ('99' -> 99 not 1999); edge-year tests probing calendar boundaries; default sentinel 0 flowing into fromisocalendar.

Related errors


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