A subscript sees __getitem__ only through a named receiver
class C:
def __init__(self, v): self.v = v
def __getitem__(self, i): return self.v + i
def mk(v): return C(v)
c = C(5); print(c[2]) # 7 — the shape that worked
print(mk(5)[2]) # CPython 7 — pxx printed 137517835747360
print(C(5)[2]) # CPython 7 — pxx: error: unexpected token
xs = [mk(1)]; print(xs[0][3])
# CPython 4 — pxx: TypeError: object is not subscriptable
Found 2026-08-15 by a CPython differential sweep of the dunder surface. The
first row is the bad one: a plausible large integer, printed where an element
was expected, with no diagnostic — devdocs/dev/debugging-playbook.md's opening
failure class.
One protocol, four receiver shapes, one of them wired
ParseLValueAST's suffix loop has dispatched __getitem__ since the protocol
landed. The other three paths had not:
| receiver | before |
|---|---|
a NAME — c[2] |
correct |
a call result / chain — mk(5)[2], o.child()[k] |
raw AN_INDEX over the instance handle → the pointer |
a CONSTRUCTION — C(5)[2] |
parse error: the ctor arm applied no selectors |
a VARIANT holding the object — xs[0][3], a for-loop variable |
run-time TypeError |
PyParseClassRecordSelectors — the chained loop — now dispatches both halves of
the protocol (read and, by peeking past the balanced brackets for a trailing
=, write), exactly as its default-property arm already did for Pascal; the
NilPy ctor arm hands a following [ to that same loop, the way the Pascal cast
arm hands over its own result; and pyvar_getitem consults __getitem__
through PyUserArithCall1 when the variant holds a user object, so the shape
with no static class agrees too. That function's header already records this
same "two parsers by receiver shape" failure twice for other members
(project_nilpy_lvalue_vs_selector_path_must_both_know).
A class declaring only __setitem__ keeps the run-time TypeError on a READ, and
one declaring neither is untouched — the named path's trades, now shared.
Not this bug, confirmed by measurement
obj[2:3] on a user class does not parse for a NAMED receiver either, so it is
not a receiver-shape gap: it needs slice objects, which this dialect does not
have ([[bug-nilpy-four-remaining-absent-builtins]]). __delitem__ through a
variant likewise stays a loud refusal — it needs a 3-argument dispatcher that
does not exist, as pyvar_delitem's own comment says.
Gate
test/test_nilpy_getitem_on_a_call_result.npy (+.expected, in the Makefile),
byte-identical to CPython: a named receiver as the control; a call result, a
construction, a chained call, and a subscript whose KEY is itself a subscripted
call; two subscripted call results added together (so a re-evaluated receiver
would show); the object held in a list, in a dict value and as a for-loop
variable; and __setitem__ through a name and through a call result.
gate.sh quick GREEN, pinned v330.