Skip to content

[Bug]: Assorted CPython compatibility divergences (15 items) #1291

Open

Description

@jseop-lim

Describe the bug

Found by differential testing against CPython 3.12. All 15 items reproduce on GraalPy 25.4.4.1.1, and CPython 3.12.13 and 3.13.16 give the same expected output for each. Every item under "Steps to reproduce" has a reproduction, both outputs and the cause, with a suggested fix folded underneath.

# API GraalPy CPython
1 callable(classmethod(f)) True False
2 type('X', (3,), {}) UnsupportedSpecializationException, exit 1 TypeError
3 vars(type('C', (), {})) key order __module__ third __module__ first
4 NotImplementedType.__bool__ missing present, warns
5 frozenset(F()) with a subclass instance F frozenset
6 repr(5e-324) 4.9e-324 5e-324
7 compile(..., optimize=5) TypeError ValueError
8 pow(1, int, 1j) ValueError TypeError
9 list subclass + list subclass list
10 bytearray item assignment out of range IndexError TypeError, ValueError
11 math.gcd / math.lcm with one __index__ argument TypeError int
12 itertools.combinations(1, -1), combinations_with_replacement(1, -1) ValueError TypeError
13 itertools.count(True) count(True) count(1)
14 functools.lru_cache(typed='x') TypeError accepted
15 bisect.bisect_right / bisect_left with hi=-1 0 len(a) used

Operating system

Linux

CPU architecture

ARM64

GraalPy version

25.4.4.1.1 (Python 3.13.14); code read on master c1c5cef

JDK version

No response

Context configuration

No response

Steps to reproduce

1. classmethod objects are callable

cm = classmethod(lambda: 1)
Expression GraalPy CPython
callable(cm) True False
iter(cm, None) <callable_iterator object at 0x...> TypeError: iter(v, w): v must be callable
cm() 1 TypeError: 'classmethod' object is not callable

ClassmethodCommonBuiltins.CallNode registers tp_call in the slots that classmethod shares with classmethod_descriptor (ClassmethodCommonBuiltins.java#L126-L135). In CPython PyClassMethod_Type has no tp_call (funcobject.c#L1139); only classmethod_descriptor is callable.

Fix suggestion

Move CallNode from ClassmethodCommonBuiltins to BuiltinClassmethodBuiltins, so only classmethod_descriptor gets tp_call.

2. type() with a non-type base crashes when the error message is read

try:
    type('X', (3,), {})
except TypeError as e:
    print('TypeError:', e)

GraalPy:

com.oracle.truffle.api.dsl.UnsupportedSpecializationException: Unexpected values provided for TypeNodesFactory.GetTpNameNodeGen.Uncached@5009421e: [null, 3], [null,Integer]

CPython:

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

calculateMetaclass (TypeBuiltins.java#L401-L423) checks IsAcceptableBaseNode on each base before comparing metaclasses, and raises TYPE_IS_NOT_ACCEPTABLE_BASE_TYPE, whose %N needs a type, for the int 3. Formatting the message calls GetTpNameNode on a non-type, which has no specialization for it. CPython's _PyType_CalculateMetaclass (typeobject.c#L3057-L3089) compares Py_TYPE(base) first, so a non-type base always gives the metaclass conflict error. Bases such as len, lambda: 0 or functools.partial(len) crash the same way.

Fix suggestion

In calculateMetaclass, move the IsAcceptableBaseNode check into a separate pass after the metaclass loop, as CPython runs best_base after _PyType_CalculateMetaclass.

3. type() puts __module__ after the __dict__ and __weakref__ descriptors

Expression GraalPy CPython
list(vars(type('C', (), {}))) ['__dict__', '__weakref__', '__module__', '__doc__'] ['__module__', '__dict__', '__weakref__', '__doc__']

CreateTypeNode.makeType (TypeNodes.java#L1865-L1876) creates the class, which installs the descriptors, and only then sets __module__. CPython sets __module__ (typeobject.c#L3674) before PyType_Ready adds the descriptors. A class statement is not affected because its namespace already holds __module__.

Fix suggestion

In makeType, put __module__ into namespace before calling typeMetaclass.execute().

4. NotImplementedType has no __bool__

import warnings
warnings.simplefilter('error')
Expression GraalPy CPython
'__bool__' in dir(NotImplemented) False True
bool(NotImplemented) True DeprecationWarning: NotImplemented should not be used in a boolean context

NotImplementedBuiltins (NotImplementedBuiltins.java#L67-L107) defines no nb_bool. CPython's notimplemented_bool (object.c#L1962-L1975) warns and returns true.

Fix suggestion

Add an nb_bool slot to NotImplementedBuiltins that issues the DeprecationWarning and returns true.

5. frozenset() returns a frozenset subclass instance unchanged

class F(frozenset):
    pass
Expression GraalPy CPython
type(frozenset(F())) <class '__main__.F'> <class 'frozenset'>

frozensetIdentity (FrozenSetBuiltins.java#L104-L109) returns the argument for any PFrozenSet. CPython takes that shortcut only for an exact frozenset (setobject.c#L995).

Fix suggestion

Guard frozensetIdentity on the argument being an exact frozenset, and let a subclass instance fall through to the copying specialization.

6. repr() of the smallest subnormals is not the shortest string

Expression GraalPy CPython
repr(5e-324) 4.9e-324 5e-324
repr(1e-323) 9.9e-324 1e-323
repr(5e-323) 4.9e-323 5e-323

reprDigits (FloatFormatter.java#L775-L778) takes its digits from Double.toString, which picks the closest decimal of length 1 or 2 when one digit would round-trip. CPython's float_repr uses the shortest string that round-trips (floatobject.c#L391-L394).

Fix suggestion

In reprDigits, for a subnormal value use the nearest one-digit decimal (new BigDecimal(value).round(new MathContext(1, RoundingMode.HALF_EVEN))) when it round-trips. Simulated in plain Java, this matches CPython on the first 300,000 positive subnormals.

7. compile() raises TypeError for an out-of-range optimize

Expression GraalPy CPython
compile('1', 'f', 'exec', optimize=5) TypeError: compile(): invalid optimize value ValueError: compile(): invalid optimize value

checkOptimize (BuiltinFunctions.java#L1137-L1142) raises TypeError. CPython raises ValueError (bltinmodule.c#L766-L770).

Fix suggestion

Raise ValueError in checkOptimize, as the neighbouring checkFlags does.

8. Three-argument pow() raises ValueError for operands complex cannot convert

Expression GraalPy CPython
pow(1, int, 1j) ValueError: complex modulo TypeError: unsupported operand type(s) for ** or pow(): 'int', 'type', 'complex'
pow(1j, set(), 3) ValueError: complex modulo TypeError: unsupported operand type(s) for ** or pow(): 'complex', 'set', 'int'

ComplexBuiltins.PowerNode.error (ComplexBuiltins.java#L1192-L1198) raises as soon as a modulus is given. CPython's complex_pow (complexobject.c#L507-L517) converts both operands first and returns NotImplemented when one is not a number.

Fix suggestion

In error, convert left and right with ToComplexValueNode and return NotImplemented when either fails, raising ValueError only otherwise.

9. list subclass + list returns the subclass

import operator

class L(list):
    pass
Expression GraalPy CPython
type(L([1]) + [2]) <class '__main__.L'> <class 'list'>
type(operator.concat(L([1]), [2])) <class '__main__.L'> <class 'list'>

ListBuiltins.ConcatNode allocates the result with the class from GetClassForNewListNode (ListNodes.java#L196-L204), which is the receiver's class. CPython's list_concat always builds an exact list (listobject.c#L531). The result skips L.__init__.

Fix suggestion

Have GetClassForNewListNode.doPList return PythonBuiltinClassType.PList, which also fixes L.copy().

10. bytearray item assignment checks the index before the value

b = bytearray(b'x')
Expression GraalPy CPython
b[5] = 1j IndexError: bytearray index out of range TypeError: 'complex' object cannot be interpreted as an integer
b[5] = 300 IndexError: bytearray index out of range ValueError: byte must be in range(0, 256)

SetSubscriptNode.set and SetItemNode.set (ByteArrayBuiltins.java#L221-L263) bounds-check the index before the value is converted. CPython converts the value first (bytearrayobject.c#L606-L616, GH-91153).

Fix suggestion

In both set methods, convert value to a byte before normalizing the index, then store the converted byte.

11. math.gcd() and math.lcm() with one argument call abs() before __index__

import math

class Idx:
    def __index__(self):
        return 6
Expression GraalPy CPython
math.gcd(Idx()) TypeError: bad operand type for abs(): 'Idx' 6
math.lcm(Idx()) TypeError: bad operand type for abs(): 'Idx' 6

The one-argument specializations GcdNode.gcdOne and LcmNode.gcdOne (MathModuleBuiltins.java#L969-L975, #L1125-L1131) both compute index(abs(x)). CPython computes abs(index(x)) (mathmodule.c#L767-L773, #L842-L848).

Fix suggestion

Swap the two calls in both GcdNode.gcdOne and LcmNode.gcdOne.

12. itertools.combinations() and combinations_with_replacement() check r before the iterable

Expression GraalPy CPython
itertools.combinations(1, -1) ValueError: r must be non-negative TypeError: 'int' object is not iterable
itertools.combinations_with_replacement(1, -1) ValueError: r must be non-negative TypeError: 'int' object is not iterable

Both constructors (CombinationsBuiltins.java#L134-L139, CombinationsWithReplacementBuiltins.java#L109-L113) raise for r < 0 before converting the iterable. CPython builds the pool first (itertoolsmodule.c#L2577-L2583, #L2893-L2899).

Fix suggestion

In both construct methods, call toArrayNode.execute(frame, iterable) before the r < 0 check, as PermutationsBuiltins does.

13. itertools.count() keeps an int subclass start

Expression GraalPy CPython
repr(itertools.count(True)) count(True) count(1)
next(itertools.count(True)) True 1

CountNode.construct (CountBuiltins.java#L114-L133) stores start as passed. CPython's fast mode (itertoolsmodule.c#L4028-L4067) keeps an int start with step 1 as a C Py_ssize_t, so it produces exact ints.

Fix suggestion

In construct, when start and step are int instances, step == 1 and start fits in a long, store start as an exact int.

14. functools.lru_cache() requires an integer typed

import functools
Expression GraalPy CPython
functools.lru_cache(maxsize=2, typed='x')(len)('ab') TypeError: 'str' object cannot be interpreted as an integer 2

LruCacheWrapperBuiltins converts typed with ClinicConversion.Int (LruCacheWrapperBuiltins.java#L123). CPython parses it with p (_functoolsmodule.c#L1168).

Fix suggestion

Use ClinicConversion.Boolean for typed and store typed ? 1 : 0.

15. bisect functions treat an explicit hi=-1 as an empty range

import bisect
Expression GraalPy CPython
bisect.bisect_right([1, 2], 1, 0, -1) 0 1
bisect.bisect_left([1, 2], 3, 0, -1) 0 2
a = [1, 2]; bisect.insort_right(a, 3, 0, -1); a [3, 1, 2] [1, 2, 3]

GraalPy has no _bisect, so bisect keeps the pure-Python functions from the bundled bisect.py (bisect.py#L111-L114), which replace only hi is None with len(a) (#L36, #L89). CPython runs the C _bisect, where -1 is the default of hi and becomes len(a) (_bisectmodule.c#L60-L64, #L244-L248). CPython declined to change its pure-Python version for this (python/cpython#125889).

Fix suggestion

Provide a native _bisect with the C argument handling, or treat hi == -1 like None in bisect_right and bisect_left of the bundled bisect.py.

Expected behavior

The CPython column of each item. CPython 3.12.13 and 3.13.16 give the same output for all of them.

Stack trace

Additional context

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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