pola-rs/polars · error

first cast to integer before applying modulo on datelike dty

Error message

first cast to integer before applying modulo on datelike dtypes

What it means

Raised by Series.__mod__ when applying `%` to any temporal Series, including Duration (there is no Duration exemption here). Modulo on a datelike value is ambiguous - there is no defined unit - so Polars asks you to cast to an integer or extract the component you actually want the remainder of (hour, weekday, etc.).

Source

Thrown at py-polars/src/polars/series/series.py:1340

        ):
            return self.to_frame().select(F.col(self.name) * other).to_series()
        elif isinstance(other, pl.DataFrame):
            return other * self
        else:
            return self._arithmetic(other, "mul", "mul_<>")

    @overload
    def __mod__(self, other: Expr) -> Expr: ...

    @overload
    def __mod__(self, other: Any) -> Series: ...

    def __mod__(self, other: Any) -> Series | Expr:
        if isinstance(other, pl.Expr):
            return F.lit(self).__mod__(other)
        if self.dtype.is_temporal():
            msg = "first cast to integer before applying modulo on datelike dtypes"
            raise TypeError(msg)
        if self.dtype.is_decimal() and isinstance(other, (float, int)):
            return self.to_frame().select(F.col(self.name) % other).to_series()
        return self._arithmetic(other, "rem", "rem_<>")

    def __rmod__(self, other: Any) -> Series:
        if self.dtype.is_temporal():
            msg = "first cast to integer before applying modulo on datelike dtypes"
            raise TypeError(msg)
        return self._arithmetic(other, "rem", "rem_<>_rhs")

    def __radd__(self, other: Any) -> Series:
        if isinstance(other, str) or (
            isinstance(other, (int, float)) and self.dtype.is_decimal()
        ):
            return self.to_frame().select(other + F.col(self.name)).to_series()
        return self._arithmetic(other, "add", "add_<>_rhs")

    def __rsub__(self, other: Any) -> Series:

View on GitHub (pinned to df599052da)

Solutions

  1. Extract the component first, then mod: `s.dt.hour() % 12`, `s.dt.weekday() % 7`, `s.dt.ordinal_day() % 7`.
  2. Cast to integer if you truly want the raw epoch remainder: `s.cast(pl.Int64) % n`.
  3. For durations, use totals: `s.dt.total_hours() % 24`.
  4. In expressions/DataFrames the same rule applies - keep the mod on the extracted numeric column.

Example fix

// before
dt = pl.Series([datetime(2024,1,1,13)]).cast(pl.Datetime)
dt % 12  # TypeError

// after
dt.dt.hour() % 12  # 1
Defensive patterns

Strategy: type-guard

Validate before calling

if s.dtype.is_temporal():
    raise SystemExit(f'refusing mod on {s.dtype}; extract a component first')
result = s % n

Type guard

def supports_mod(s: pl.Series) -> bool:
    return not s.dtype.is_temporal()

Try / catch

try:
    out = s % n
except TypeError as e:
    if 'datelike' not in str(e):
        raise
    out = s.cast(pl.Int64) % n

Prevention

When it happens

Trigger: Calling `%` on a Series with self.dtype.is_temporal() and a non-Expr right operand: `datetime_series % 2`, `duration_series % 7`, `date_series % s_int`.

Common situations: Cyclical-time features in ML pipelines ('hour % 12', 'day_of_year % 7'); porting pandas/numpy code that applied modulo directly to datetime64 columns; wrap-around bucketing of durations.

Related errors


AI-assisted analysis of pola-rs/polars@df599052da (2026-08-16). Data as JSON: /api/errors/b94ac76c53c97fae. Report an issue: GitHub.