pandas-dev/pandas · error · ValueError

Must specify a valid frequency

Error message

Must specify a valid frequency: {freq}

What it means

Raised by ArrowExtensionArray._round_temporally when `pandas.tseries.frequencies.to_offset(freq)` returns None, meaning the value cannot be parsed as a valid frequency/offset alias. The check happens before the supported-prefix lookup, so even valid-but-unsupported offsets never reach the later 'not supported' message.

Solutions

  1. Use a valid frequency alias such as 'h', 'D', 'min', 's', 'W', 'M', 'Q', or 'Y' (optionally with a multiplier like '2h').
  2. If constructing freq programmatically, validate it with `pd.tseries.frequencies.to_offset(freq)` first and handle None.

Example fix

# before
ser.dt.floor('xyz')
# after
ser.dt.floor('h')
Defensive patterns

Strategy: validation

Validate before calling

from pandas.tseries.frequencies import to_offset
if to_offset(freq) is None:
    raise ValueError(f"invalid frequency: {freq!r}")
ser.dt.floor(freq)

Type guard

def is_valid_freq(freq) -> bool:
    from pandas.tseries.frequencies import to_offset
    return to_offset(freq) is not None

Try / catch

try:
    out = ser.dt.floor(freq)
except ValueError as e:
    if "valid frequency" in str(e):
        out = ser.dt.floor('h')   # use a safe default
    else:
        raise

Prevention

When it happens

Trigger: Calling `.dt.floor/.dt.ceil/.dt.round` on a timestamp[pyarrow] Series with a value that is not a frequency string or DateOffset, e.g. `ser.dt.floor('xyz')`, `ser.dt.floor(5)`, or `ser.dt.floor('')`.

Common situations: Typos in frequency aliases; passing a bare integer thinking it means 'N seconds'; building the freq dynamically from unvalidated input.

Related errors


AI-assisted analysis of pandas-dev/pandas@3b7651241d (2026-08-11). Data as JSON: /api/errors/3193d7aa6d98fdf4. Report an issue: GitHub.

Appendix: source

Thrown at pandas/core/arrays/arrow/array.py:4205

    def _dt_strftime(self, format: str) -> Self:
        result = pc.strftime(self._pa_array, format=format)
        return self._from_pyarrow_array(result)

    def _round_temporally(
        self,
        method: Literal["ceil", "floor", "round"],
        freq,
        ambiguous: TimeAmbiguous = "raise",
        nonexistent: TimeNonexistent = "raise",
    ) -> Self:
        if ambiguous != "raise":
            raise NotImplementedError("ambiguous is not supported.")
        if nonexistent != "raise":
            raise NotImplementedError("nonexistent is not supported.")
        offset = to_offset(freq)
        if offset is None:
            raise ValueError(f"Must specify a valid frequency: {freq}")
        pa_supported_unit = {
            "Y": "year",
            "YS": "year",
            "Q": "quarter",
            "QS": "quarter",
            "M": "month",
            "MS": "month",
            "W": "week",
            "D": "day",
            "h": "hour",
            "min": "minute",
            "s": "second",
            "ms": "millisecond",
            "us": "microsecond",
            "ns": "nanosecond",
        }
        unit = pa_supported_unit.get(offset._prefix, None)
        if unit is None:

View on GitHub (pinned to 3b7651241d)