Description
tuple(obj) starts iterating before validating __len__ / __length_hint__, so any exception raised by __getitem__ propagates instead of the TypeError / ValueError that CPython raises when the length hint is invalid. This is a CPython compatibility issue.
Root Cause
ConstructTupleNode.generic() obtains an iterator via PyObjectGetIter and immediately hands it to CreateStorageFromIteratorNode without passing a length hint. __len__ / __length_hint__ are never validated, so the Exception raised by __getitem__ during iteration propagates unchanged.
CPython's PySequence_Tuple instead calls PyObject_LengthHint(v, 10) before iteration, which validates the return value of __len__ / __length_hint__ and raises TypeError / ValueError first — before __getitem__ is ever invoked.
CPython reference: Objects/abstract.c#L91-L146, Objects/abstract.c#L2053
// PyObject_LengthHint validates __len__ / __length_hint__ before any iteration.
// __len__ path — a negative return value raises ValueError("__len__() should return >= 0").
if (_PyObject_HasLen(o)) {
res = PyObject_Length(o);
if (res < 0) {
if (!_PyErr_ExceptionMatches(tstate, PyExc_TypeError)) {
return -1; // propagate non-TypeError (e.g. ValueError) as-is
}
_PyErr_Clear(tstate);
} else {
return res;
}
}
// __length_hint__ path — a non-int return value raises TypeError.
if (!PyLong_Check(result)) {
PyErr_Format(PyExc_TypeError, "__length_hint__ must be an integer, not %.100s",
Py_TYPE(result)->tp_name);
return -1;
}
Reproduction
class BadLengthHint:
def __getitem__(self, index):
raise Exception
def __length_hint__(self):
return None
tuple(BadLengthHint())
Output
GraalPy:
CPython:
TypeError: __length_hint__ must be an integer, not NoneType
Environment
- GraalPy 25.0.2 (Python 3.12.8)
- CPython v3.12.13
- OS: Debian 12
Additional context
The same GraalPy already does this correctly on the list(iterable) path (ListBuiltins.listIterable), which uses IteratorNodes.GetLength. Only the tuple() path is missing the pre-validation.
|
static PNone listIterable(VirtualFrame frame, PList list, Object iterable, |
|
@Bind Node inliningTarget, |
|
// exclusive for truffle-interpreted-performance |
|
@Exclusive @Cached ClearListStorageNode clearStorageNode, |
|
@Cached IteratorNodes.GetLength lenNode, |
|
@Cached PyObjectGetIter getIter, |
|
@Cached CreateStorageFromIteratorNode storageNode) { |
|
clearStorageNode.execute(inliningTarget, list); |
|
int len = lenNode.execute(frame, inliningTarget, iterable); |
|
Object iterObj = getIter.execute(frame, inliningTarget, iterable); |
|
list.setSequenceStorage(storageNode.execute(frame, iterObj, len)); |
|
return PNone.NONE; |
|
} |
Applying the list()-style fix directly did not work because of the @Fallback(excludeForUncached = true) annotation on ConstructTupleNode.generic(), so this is being filed as an issue only.
Description
tuple(obj)starts iterating before validating__len__/__length_hint__, so any exception raised by__getitem__propagates instead of theTypeError/ValueErrorthat CPython raises when the length hint is invalid. This is a CPython compatibility issue.Root Cause
ConstructTupleNode.generic()obtains an iterator viaPyObjectGetIterand immediately hands it toCreateStorageFromIteratorNodewithout passing a length hint.__len__/__length_hint__are never validated, so theExceptionraised by__getitem__during iteration propagates unchanged.CPython's
PySequence_Tupleinstead callsPyObject_LengthHint(v, 10)before iteration, which validates the return value of__len__/__length_hint__and raisesTypeError/ValueErrorfirst — before__getitem__is ever invoked.Reproduction
Output
GraalPy:
CPython:
Environment
Additional context
The same GraalPy already does this correctly on the
list(iterable)path (ListBuiltins.listIterable), which usesIteratorNodes.GetLength. Only thetuple()path is missing the pre-validation.graalpython/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/list/ListBuiltins.java
Lines 267 to 279 in 4f02c3d
Applying the
list()-style fix directly did not work because of the@Fallback(excludeForUncached = true)annotation onConstructTupleNode.generic(), so this is being filed as an issue only.