pandas-dev/pandas · error · ValueError

start and end must not be NaT

Error message

start and end must not be NaT

What it means

Raised in _get_ordinal_range when either `start` or `end` resolved to pandas NaT. A period range cannot be generated from an unparseable or missing temporal bound, so pandas aborts rather than producing a garbage range. This is the explicit guard before it would try to read .ordinal off a NaT.

Source

Thrown at pandas/core/arrays/period.py:1536

            "exactly two must be specified"
        )

    if freq is not None:
        freq = to_offset(freq, is_period=True)
        mult = freq.n

    if start is not None:
        start = Period(start, freq)
    if end is not None:
        end = Period(end, freq)

    is_start_per = isinstance(start, Period)
    is_end_per = isinstance(end, Period)

    if is_start_per and is_end_per and start.freq != end.freq:
        raise ValueError("start and end must have same freq")
    if start is NaT or end is NaT:
        raise ValueError("start and end must not be NaT")

    if freq is None:
        if is_start_per:
            freq = start.freq
        elif is_end_per:
            freq = end.freq
        else:  # pragma: no cover
            raise ValueError("Could not infer freq from start/end")
        mult = freq.n

    if periods is not None:
        periods = periods * mult
        if start is None:
            data = np.arange(
                end.ordinal - periods + mult, end.ordinal + 1, mult, dtype=np.int64
            )
        else:
            data = np.arange(

View on GitHub (pinned to 71959b8cb9)

Solutions

  1. Validate start/end with pd.isna() before calling period_range and skip/handle the empty case.
  2. Sanitize inputs through pd.to_datetime(..., errors='coerce') then dropna() so NaT never reaches period_range.
  3. If the NaT comes from a config/UI field, require a valid date and surface a form error instead of calling the API.

Example fix

// before
rng = pd.period_range(start=df.loc[i,'from'], end=df.loc[i,'to'], freq='D')
// after
s, e = df.loc[i,'from'], df.loc[i,'to']
rng = pd.period_range(start=s, end=e, freq='D') if pd.notna(s) and pd.notna(e) else pd.PeriodIndex([], freq='D')
Defensive patterns

Strategy: validation

Validate before calling

import pandas as pd

def period_range_or_empty(start, end, freq):
    if pd.isna(start) or pd.isna(end):
        return pd.PeriodIndex([], freq=freq)
    return pd.period_range(start=start, end=end, freq=freq)

Type guard

def is_valid_period_bound(x) -> bool:
    return x is not None and not pd.isna(x)

Try / catch

try:
    rng = pd.period_range(start=start, end=end, freq=freq)
except ValueError as e:
    if 'must not be NaT' in str(e):
        rng = pd.PeriodIndex([], freq=freq)
    else:
        raise

Prevention

When it happens

Trigger: period_range(start=pd.NaT, end='2020', freq='D'); passing a column value that parsed to NaT (e.g. an empty string or invalid date string) as start or end; start=pd.Timestamp('NaT') or a NaT produced by a failed to_datetime conversion.

Common situations: Dates read from dirty CSVs where some cells are blanks/NaN; downstream code that forwards user-supplied 'from'/'to' filters without validating them; timezone or format mismatches in to_datetime that silently yield NaT.

Related errors


AI-assisted analysis of pandas-dev/pandas@71959b8cb9 (2026-08-07). Data as JSON: /api/errors/257a62cf3a6f714b. Report an issue: GitHub.