Describe the bug
Two problems in the same tp_new specialization set. An explicit start=None is treated as if the argument had been omitted, so enumerate(iterable, None) builds an object with start=0 instead of raising. And a start that is not already an int is rejected without coercion, so bool and any object implementing __index__ raise TypeError where CPython accepts them.
Operating system
Linux
CPU architecture
ARM64
GraalPy version
25.2.4 (Python 3.12.8); also reproduced on 25.0.2
JDK version
No response
Context configuration
No response
Steps to reproduce
An explicit None:
print(enumerate("a", None))
GraalPy 25.2.4:
<enumerate object at 0x2671a8f>
A start that needs coercion:
class Three:
def __index__(self):
return 3
print(list(enumerate([9, 8, 7], Three())))
print(list(enumerate("abc", True)))
GraalPy 25.2.4:
Traceback (most recent call last):
File "/tmp/a.py", line 5, in <module>
print(list(enumerate([9, 8, 7], Three())))
^^^^^^^^^^^^^^^^^^
TypeError: 'Three' object cannot be interpreted as an integer
enumerate("abc", True) raises the same way, with 'bool' as the type name.
Expected behavior
CPython 3.12.13 raises on the first, and accepts both starts on the second:
TypeError: 'NoneType' object cannot be interpreted as an integer
[(3, 9), (4, 8), (5, 7)]
[(1, 'a'), (2, 'b'), (3, 'c')]
Additional context
The rejecting specialization claims an omitted argument too: its guard is !isIntegerIndex(start), and PNone.NO_VALUE is not an integer index either. Specialization state is shared across call sites, so on 25.2.4 a call that omits start raises once an earlier one was rejected for a bad start:
try:
enumerate("abc", "x")
except TypeError:
pass
print(list(enumerate("abc")))
GraalPy 25.2.4: TypeError: 'NoneType' object cannot be interpreted as an integer
CPython 3.12.13: [(0, 'a'), (1, 'b'), (2, 'c')]
Root Cause
The explicit None. An omitted start arrives as PNone.NO_VALUE, an explicit start=None as PNone.NONE. EnumerateBuiltins.java#L86-L92 dispatches doNone on the Java type PNone, which matches both singletons, so both give start=0. CPython keeps them apart in enum_new_impl, where start is NULL only if the argument was absent:
if (start != NULL) {
start = PyNumber_Index(start);
...
} else {
en->en_index = 0;
getattr gets the same distinction right with isNoValue(defaultValue) at BuiltinFunctions.java#L1265.
The rejected starts. EnumerateBuiltins.java#L118-L126 decides acceptance with a Java type test — isIntegerIndex admits only Integer, Long, and PInt — and everything else raises without the object being consulted. CPython runs the argument through PyNumber_Index, which takes bool on the PyLong_Check fast path (PyBool_Type.tp_base is PyLong_Type) and calls nb_index on anything else providing it. range already does this in GraalPy — RangeBuiltins.java#L163-L175 coerces through PyNumberIndexNode, and range(True) / range(Three()) both work.
Fix Suggestion
Guard the default specialization so it matches only an omitted argument, and let a start that was actually passed go through PyNumberIndexNode rather than a type test — the same shape range already uses. Both halves are needed together: the specializations are shared, so one alone moves the failure rather than removing it.
Describe the bug
Two problems in the same
tp_newspecialization set. An explicitstart=Noneis treated as if the argument had been omitted, soenumerate(iterable, None)builds an object withstart=0instead of raising. And astartthat is not already anintis rejected without coercion, sobooland any object implementing__index__raiseTypeErrorwhere CPython accepts them.Operating system
Linux
CPU architecture
ARM64
GraalPy version
25.2.4 (Python 3.12.8); also reproduced on 25.0.2
JDK version
No response
Context configuration
No response
Steps to reproduce
An explicit
None:GraalPy 25.2.4:
A
startthat needs coercion:GraalPy 25.2.4:
enumerate("abc", True)raises the same way, with'bool'as the type name.Expected behavior
CPython 3.12.13 raises on the first, and accepts both starts on the second:
Additional context
The rejecting specialization claims an omitted argument too: its guard is
!isIntegerIndex(start), andPNone.NO_VALUEis not an integer index either. Specialization state is shared across call sites, so on 25.2.4 a call that omitsstartraises once an earlier one was rejected for a badstart:GraalPy 25.2.4:
TypeError: 'NoneType' object cannot be interpreted as an integerCPython 3.12.13:
[(0, 'a'), (1, 'b'), (2, 'c')]Root Cause
The explicit
None. An omittedstartarrives asPNone.NO_VALUE, an explicitstart=NoneasPNone.NONE.EnumerateBuiltins.java#L86-L92dispatchesdoNoneon the Java typePNone, which matches both singletons, so both givestart=0. CPython keeps them apart inenum_new_impl, wherestartisNULLonly if the argument was absent:getattrgets the same distinction right withisNoValue(defaultValue)atBuiltinFunctions.java#L1265.The rejected starts.
EnumerateBuiltins.java#L118-L126decides acceptance with a Java type test —isIntegerIndexadmits onlyInteger,Long, andPInt— and everything else raises without the object being consulted. CPython runs the argument throughPyNumber_Index, which takesboolon thePyLong_Checkfast path (PyBool_Type.tp_baseisPyLong_Type) and callsnb_indexon anything else providing it.rangealready does this in GraalPy —RangeBuiltins.java#L163-L175coerces throughPyNumberIndexNode, andrange(True)/range(Three())both work.Fix Suggestion
Guard the default specialization so it matches only an omitted argument, and let a
startthat was actually passed go throughPyNumberIndexNoderather than a type test — the same shaperangealready uses. Both halves are needed together: the specializations are shared, so one alone moves the failure rather than removing it.