How it was reached
Answering the "worth checking in the same place" list at the end of
bug-n-arithmetic-on-a-user-class-fails-when-the-other-operand-is-object-typed.
__call__ was on that list and turned out not to belong to it: it fails with an
ordinary float literal too, so it is not the object-typed-operand bug and gets
its own file.
Repro
One file, no imports, no lambda.
class V:
def __init__(self, x=0.0):
self.x = x
def __call__(self, a):
return "CALL"
v = V(22.0)
def t1():
return v(1.0)
def t3():
if True:
return v(1.0)
return None
def t7():
return v(1.0), 1
def verdict(tag, got):
if isinstance(got, str):
print(tag, "-> ok:", got)
else:
print(tag, "-> WRONG: constructed a V instead of calling __call__")
verdict("t1 bare return ", t1())
verdict("t3 return inside `if True`", t3())
verdict("t7 in a tuple ", t7()[0])
verdict("t8 module level ", v(1.0))
The table
v is a module-level V(22.0); __call__ returns the string "CALL".
| shape | CPython | pxx @ c53d9ab55 |
|---|---|---|
return v(1.0) -- first def in the module |
CALL |
new V |
return v(1.0) -- second def, identical body |
CALL |
new V |
x = v(1.0) then return x |
CALL |
new V |
return v(1.0) in a def that takes a parameter |
CALL |
new V |
r = None first, then r = v(1.0) |
CALL |
new V |
if True: return v(1.0) |
CALL |
CALL |
return v(1.0), 1 -- a tuple element |
CALL |
CALL |
v(1.0) at module level |
CALL |
CALL |
f = v first, then f(1.0) |
CALL |
CALL |
| instance created locally in the function | CALL |
CALL |
| instance arriving as a PARAMETER | CALL |
CALL |
Measured at c53d9ab55, compiler binary 2026-09-14, x86-64 --threadsafe.
Readings:
- The escape is nesting, not scope.
if True: return v(1.0)is the same expression in the same function reading the same global, one block deeper, and it is correct. So the name resolves correctly in that scope; something about the call appearing as a top-level statement expression in the function body picks the class instead. - Being a sub-expression also escapes it.
return v(1.0), 1is correct. Together with (1) this looks like a resolution done at the statement level rather than the expression level. - It is the GLOBAL that is required. A local instance, an aliased local, and an instance arriving as a parameter are all correct. Only the module-level name is mis-resolved.
- Case is not involved. I checked the obvious Pascal-host hypothesis --
vversusVcolliding in a case-insensitive symbol table -- and it is wrong:inst = V(...)andgadget = Thing(...), names sharing nothing with their class, behave identically tovandthing. Recording the negative so nobody else spends a build on it.
What it looks like when it bites
Silently, and then somewhere else. The first way I saw it was not a wrong value but a runtime abort three lines later:
Nil Python: f-string format spec ".1f" on a value of variant tag 41 is not supported
which was __repr__ running "V(%.1f)" % self.x on the wrongly-constructed
instance, whose x had been handed the call's argument through the wrong
signature. The reported error names a format spec in a method that is entirely
innocent; nothing points at the call site.
Where to look
The call lowering for a bare NAME(args) statement expression in a function
body. The correct rows say the machinery exists and is reached from every other
position, so this reads like one arm resolving NAME against the type table
before the module-globals table, rather than a missing feature.
Gate
make test-nilpy + self-host byte-identical, plus a .npy carrying the eleven
rows above with expectations from CPython. The six correct rows matter as much
as the five wrong ones -- they are what localises the fix to the statement-level
arm instead of the call path.
Log
- 2026-09-14 -- filed while sweeping the operator matrix for the object-typed operand bug. Reduction is single-file and inline above.
Provenance
Measured and written by the peer session lekkerzeilen-c8, which cannot commit in this checkout; this file is left untracked for that seat to pick up.