pola-rs/polars · error · ValueError

negative stride is not supported in conjunction with start+s

Error message

negative stride is not supported in conjunction with start+stop

What it means

Polars' LazyPolarsSlice.apply (py-polars/src/polars/_utils/slice.py:139) rejects Python slice expressions applied to a LazyFrame that use a negative step together with an explicit start and/or stop (e.g. lf[2:8:-1] or lf[:5:-1]). A LazyFrame does not know its row count before collect(), so slices that would need indexing from the end cannot be mapped to efficient lazy operations. Note lf[2::-1] is fine (handled via head+reverse); only start+stop combined with negative stride raises.

Source

Thrown at py-polars/src/polars/_utils/slice.py:142

    def apply(self, s: slice) -> LazyFrame:
        """
        Apply a slice operation.

        Note that LazyFrame is designed primarily for efficient computation and does not
        know its own length so, unlike DataFrame, certain slice patterns (such as those
        requiring negative stop/step) may not be supported.
        """
        start = s.start or 0
        step = s.step or 1

        # fail on operations that require length to do efficiently
        if s.stop and s.stop < 0:
            msg = "negative stop is not supported for lazy slices"
            raise ValueError(msg)
        if step < 0 and (start > 0 or s.stop is not None) and (start != s.stop):
            if not (start > 0 > step and s.stop is None):
                msg = "negative stride is not supported in conjunction with start+stop"
                raise ValueError(msg)

        # ---------------------------------------
        # empty slice patterns
        # ---------------------------------------
        # [:0]
        # [i:<=i]
        # [i:>=i:-k]
        if (step > 0 and (s.stop is not None and start >= s.stop)) or (
            step < 0
            and (s.start is not None and s.stop is not None and s.stop >= s.start >= 0)
        ):
            return self.obj.clear()

        # ---------------------------------------
        # straight-through mappings for "reverse"
        # and/or "gather_every"
        # ---------------------------------------
        # [:]    => clone()

View on GitHub (pinned to df599052da)

Solutions

  1. Materialize first if the data fits: lf.collect()[2:8:-1]
  2. Rewrite with lazy primitives, e.g. lf.slice(0, 8).slice(2).reverse() or lf.slice(3, 5).reverse() for [3:8:-1]
  3. For full reversal use lf[::-1] or lf.reverse(), which are supported
  4. Keep the operation lazy by composing head/tail/slice/gather_every/reverse instead of Python slicing

Example fix

# before
lf = pl.scan_csv('data.csv')
out = lf[5:20:-1]  # ValueError

# after
out = lf.slice(5, 15).reverse().collect()
Defensive patterns

Strategy: validation

Validate before calling

def lazy_slice_ok(s: slice) -> bool:
    start = s.start or 0
    step = s.step or 1
    if s.stop is not None and s.stop < 0:
        return False
    if step < 0 and (start > 0 or s.stop is not None) and start != s.stop:
        if not (start > 0 > step and s.stop is None):
            return False
    return not (start < 0 and s.stop is not None)

Try / catch

try:
    out = lf[s]
except ValueError as e:
    if 'not supported' in str(e):
        out = lf.collect()[s]
    else:
        raise

Prevention

When it happens

Trigger: Calling lf[3:0:-1], lf[:5:-1], lf[1:9:-2], or any LazyFrame __getitem__ where s.step < 0, start != stop, and (start > 0 or s.stop is not None) unless the special case start > 0 > step with s.stop is None. The same slice on a materialized DataFrame works, which surprises users.

Common situations: Porting eager DataFrame slicing code to scan_csv/scan_parquet pipelines; reversing a known window of rows in a lazy query; using generic helper functions that slice both frames with the same slice object.

Related errors


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