Skip to content

[Bug]: enumerate() ignores an explicit start=None and rejects __index__ starts #1074

Description

@jseop-lim

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions