pandas-dev/pandas · error · ValueError

start and end must have same freq

Error message

start and end must have same freq

What it means

Raised inside _get_ordinal_range (the engine behind period_range and PeriodIndex construction) when both `start` and `end` are Period objects but carry different frequencies. pandas cannot lay out a contiguous ordinal range whose endpoints live on incompatible grids, so it refuses rather than silently picking one freq. The check fires only when no explicit `freq` argument overrides the Periods' own frequencies.

Source

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

        raise ValueError(
            "Of the three parameters: start, end, and periods, "
            "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
            )

View on GitHub (pinned to 71959b8cb9)

Solutions

  1. Pass an explicit freq= to period_range so both endpoints are re-grounded to it: period_range(start, end, freq='Q').
  2. Normalize both endpoints to the same freq before calling: start = start.asfreq('Q'); end = end.asfreq('Q').
  3. If the endpoints are strings, pass freq and let pandas parse both against it instead of constructing Periods yourself.

Example fix

// before
pd.period_range(start=pd.Period('2020','A-DEC'), end=pd.Period('2020Q1','Q'))
// after
pd.period_range(start='2020', end='2020Q1', freq='Q')
Defensive patterns

Strategy: validation

Validate before calling

import pandas as pd
from pandas import Period

def safe_period_range(start, end, freq=None):
    s = Period(start, freq) if not isinstance(start, Period) else start
    e = Period(end, freq) if not isinstance(end, Period) else end
    if freq is None and s.freq != e.freq:
        freq = s.freq  # or raise a clear error
    return pd.period_range(start=start, end=end, freq=freq)

Type guard

def same_freq_period_pair(start, end) -> bool:
    return (
        isinstance(start, pd.Period)
        and isinstance(end, pd.Period)
        and start.freq == end.freq
    )

Try / catch

try:
    rng = pd.period_range(start=start, end=end, freq=freq)
except ValueError as e:
    if 'must have same freq' in str(e):
        rng = pd.period_range(start=start, end=end, freq=start.freq)
    else:
        raise

Prevention

When it happens

Trigger: Calling period_range(start=Period('2020','A-DEC'), end=Period('2020Q1','Q')) or constructing a PeriodIndex/period_range with two Period endpoints whose .freq differ and with freq=None. Because Period(end, freq) preserves end's own freq when freq is None, the mismatch surfaces here.

Common situations: Mixing annual and quarterly endpoints copied from different columns; passing a start from one df and an end inferred from another without normalizing freq; refactors that drop the explicit freq= kwarg and rely on the Period objects.

Related errors


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