An extended slice cannot be assigned
Found 2026-08-16 by a tools/pydiff.py sweep.
l = [1, 2, 3]
l[::2] = [7, 8] # CPython [7, 2, 8]
# pascal26: error (parse)
l[1:3] = [9] # works
print(l[::2]) # works — the READ side is complete
Both halves it needs already exist: pylist_slice_step reads a strided slice
and pylist_setslice writes a contiguous one. The missing piece is the
lvalue-side parse of a three-part subscript plus a pylist_setslice_step
that walks the same index sequence the reader does — and, as CPython does,
raises when the RHS length does not match the number of selected slots (that
check is what makes the strided form different from the contiguous one, which
resizes).
Low priority: strided assignment is rare in real code, and it fails LOUDLY at compile time, so nothing can be silently wrong.
Gate
A .npy diffed against CPython: forward and negative steps, a length mismatch
raising ValueError, and the contiguous forms unchanged.
Log
- 2026-09-12 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit e3dcd6a35.
Resolved 2026-09-12 — implemented, CPython-differential green
Three pylib entry points, each walking the positions its matching READ selects
via PySliceBoundsStep rather than re-derived arithmetic:
pylist_setslice_step |
l[lo:hi:step] = src |
pybytes_setslice_step |
b[lo:hi:step] = src |
pyvar_setslice_step |
target with no static type; unboxes and dispatches on the runtime type |
PyRewriteSliceAssign routes all three; the refusal is gone.
The part that would have shipped broken
The refusal this ticket describes was only reachable for a target with a
static list or bytes type. The "is this a slice-assign target" predicate
named pylist_slice_step and pybytes_slice_step and not
pyvar_slice_step, so a target with no static type never entered the store
path at all — it fell through to expression-statement parsing and died on
expected newline after statement at the =.
That is not a corner. A name first bound inside an if or try body has no
static type, and neither does a container element — d["k"][::2] = ..., which
had been separately (and wrongly) read as a parse gap, is the same cause.
Every top-level spelling compiled: l[::2], l[0::2], l[::-1],
l[0:4:2], l[1::2], l[:4:2], l[0:], l[::] — all eight probed. It
surfaced only because the fixture's try/except rows rebind their target
inside the block. A fixture written the ordinary way would have certified the
feature as complete with two thirds of it missing, so the suite now holds all
three routes on purpose: static target, variant NAME, variant RECEIVER.
The diagnostic also misreported the location, naming line 14 — the first top-level occurrence of a similar statement — while the failing line was 60.
What the gate asserts
test/test_nilpy_extended_slice_assign.npy, 15 rows, .expected generated by
python3. Forward and negative steps, the empty selection, both refusal
messages verbatim, tuple immutability, and app.py:2358's own shape (bytearray,
explicit start, omitted stop).
Positive control MEASURED: the pinned compiler answers
pascal26:14: error: expected expression on this fixture, so the row is red
pre-fix.
The self-alias rows (x[::-1] = x, legal Python) are load-bearing and that was
measured, not assumed: with both snapshot guards disabled, those two rows alone
go wrong — [1, 2, 3, 2, 1] and b'abcba' — while the other thirteen stay
green.
Two residuals, deliberately not done
- A variant source into a statically-typed target (
b[::2] = d["k"]) is refused by name. The setters take aTPyBytes/TPyListand a raw variant handed to one is read as that object's HEADER — a plausible wrong length with no diagnostic — so refusing is the honest answer, but it leaves an asymmetry: a variant TARGET accepts a variant source and a static one does not. The fix to try is routing that case throughpyvar_setslice_stepand letting the target box; the thing to verify is that the boxing passes a REFERENCE, because a copy would silently not mutate. One CPython differential answers it. b[::2] = [88, 89, 90]— CPython accepts a list of ints into a bytearray extended slice. Not implemented; unrelated to this ticket's shape.
Inert for other seats until a pin: these are compiler/builtin/** entry
points, so a $(PXX_STABLE) consumer cannot see them — the pinned binary
resolves its own snapshot builtin.