*= on a sequence yields empty
xs = [1, 2]
xs *= 2
print(xs) # CPython [1, 2, 1, 2] pxx printed nothing at all
s = "ab"
s *= 3
print(s) # CPython ababab pxx printed an empty line
Silent. Found 2026-08-15 by a CPython differential sweep of slicing and
mutation. xs = xs * 2 was correct all along, which is the tell.
Cause
x * n on a sequence is not a multiply: ParseTerm decides it with three
special arms — PyMakeListRepeat, PyMakeBytesRepeat, and the str case, which
tags the binop tyAnsiString so the IR lowers it to pystr_repeat. The three
augmented-assignment sites (bare name, lhs-expression target, and the shared
dotted-target tail in parser.inc) build an AN_BINOP from the token
PyAugBinTok returns and consult none of them, so the sequence handle went into
an arithmetic multiply.
PyAugMulNode is that decision in one place, called from all three. It answers
-1 for a pair that really is arithmetic, so every other *= keeps its path.
This is the same shape as |= on a set, whose fix note two hundred lines away
says the same thing about the same three sites —
devdocs/dev/normalise-dont-special-case.md.
Divergence recorded, not hidden
CPython's list *= MUTATES in place, so an alias taken beforehand sees the
repeat; this REBINDS. += has the in-place form already (TPyList.extend) and
*= has no in-place primitive to call yet — filed as
[[bug-nilpy-augmented-sequence-repeat-rebinds-instead-of-mutating]]. An empty
list was the strictly worse answer, so the rebind lands now and the aliasing
half is its own ticket.
Gate
test/test_nilpy_augmented_sequence_repeat.npy (+.expected, in the Makefile),
byte-identical to CPython: *= on a list, a str and bytes; the int and float
forms untouched; a FIELD target and a SUBSCRIPT target (the other two augmented
sites); a variable count; counts of 0 and 1; x = x * n still agreeing; and
+= on a list still being the in-place extend, alias included. gate.sh quick
GREEN. No pin — frontend-only.