GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Assorted CPython compatibility divergences (7 items) · Issue #9748 · RustPython/RustPython Open Labels C-compatA discrepancy between RustPython and CPythonA discrepancy between RustPython and CPython DescriptionFeatureFound by differential testing against CPython 3.12. All 7 items reproduce on RustPython 0.6.1 and on main at 3c3ffc5 (the outputs below are from main), and CPython 3.12.13, 3.14.3 and 3.15.2 give the same output for each. Every item has a reproduction, both outputs and the cause, with a suggested fix folded underneath.
1.
|
| Expression | RustPython | CPython |
|---|---|---|
'__bool__' in dir(()) |
True |
False |
bool(T((1, 2))) |
True |
False |
impl AsNumber for PyTuple (tuple.rs#L661-L672) defines a boolean slot, so a subclass's __len__ is never consulted. CPython's PyTuple_Type has no tp_as_number (tupleobject.c#L855), so truth testing falls back to __len__.
Remove impl AsNumber for PyTuple and its AsNumber entry in the pyclass(with(...)) list.
staticmethod and classmethod objects accept weak references| Expression | RustPython | CPython |
|---|---|---|
weakref.ref(staticmethod(len)) |
a weak reference | TypeError: cannot create weak reference to 'staticmethod' object |
weakref.ref(classmethod(len)) |
a weak reference | TypeError: cannot create weak reference to 'classmethod' object |
Both types are declared with HAS_WEAKREF (staticmethod.rs#L99, classmethod.rs#L93). In CPython both have tp_weaklistoffset == 0 (funcobject.c#L1149, #L1342).
Drop HAS_WEAKREF from the two pyclass flags.
int / int loses the sign of a zero result| Expression | RustPython | CPython |
|---|---|---|
operator.truediv(0, -5) |
0.0 |
-0.0 |
operator.itruediv(0, -5) |
0.0 |
-0.0 |
true_div (int.rs#L7-L13) rounds a malachite Rational, which has no signed zero. CPython's long_true_divide returns -0.0 when a zero numerator meets a negative denominator (longobject.c#L4342-L4349, #L4486-L4487). The literal 0 / -5 is constant-folded to -0.0, so only the runtime path differs.
In true_div, return -0.0 when the numerator is zero and the denominator is negative.
itertools.count() keeps an int subclass start| Expression | RustPython | CPython |
|---|---|---|
repr(itertools.count(True)) |
count(True) |
count(1) |
next(itertools.count(True)) |
True |
1 |
PyItertoolsCount::py_new (itertools.rs#L211-L228) 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. count(True, 2) keeps True on both.
In py_new, when start and step are int instances, step == 1 and start fits in isize, store start as an exact int.
unicodedata.normalize() validates form before returning an empty string| Expression | RustPython | CPython |
|---|---|---|
unicodedata.normalize('X', '') |
ValueError: invalid normalization form |
'' |
unicodedata.is_normalized('X', '') |
ValueError: invalid normalization form |
True |
form is bound as NormalizeFormArg (unicodedata.rs#L15-L30), which rejects an unknown form during argument binding, before normalize and is_normalized run. CPython returns early for an empty string before it looks at form (unicodedata.c#L935-L943, #L863-L873).
Take form as a PyStrRef in normalize and is_normalized, return early for an empty unistr, and parse the form after that.
types.TracebackType() rejects keyword argumentsimport sys import types
| Expression | RustPython | CPython |
|---|---|---|
types.TracebackType(tb_next=None, tb_frame=sys._getframe(), tb_lasti=0, tb_lineno=1) |
TypeError: traceback() takes no keyword arguments |
<traceback object at 0x...> |
PyTraceback's constructor takes type Args = (Option<PyRef<Self>>, FrameObjectRef, i32, usize) (traceback.rs#L103), and a tuple binds every element positionally. CPython's tb_new has no /, so all four parameters accept keywords (traceback.c#L65-L74).
Replace the tuple in traceback.rs with a #[derive(FromArgs)] struct whose fields tb_next, tb_frame, tb_lasti and tb_lineno are #[pyarg(any)].
pow() calls __pow__ on the wrong operand when only the modulus defines itclass C: def __pow__(self, other, mod=None): return "C.__pow__"
| Expression | RustPython | CPython |
|---|---|---|
pow(None, None, C()) |
AttributeError: __pow__ |
TypeError: unsupported operand type(s) for ** or pow(): 'NoneType', 'NoneType', 'C' |
pow('a', 2, C()) |
AttributeError: __pow__ |
TypeError: unsupported operand type(s) for ** or pow(): 'str', 'int', 'C' |
ternary_op calls the modulus's slot (vm_ops.rs#L383-L388), and number_ternary_op_wrapper! (slot.rs#L577-L587) then looks up __pow__ on the first operand, None. CPython's slot_nb_power (typeobject.c#L8600-L8613) calls __pow__ only when the first operand's type uses that slot, and returns NotImplemented otherwise.
In the three-argument branch of number_ternary_op_wrapper!, look the method up with get_special_method and return NotImplemented when the first operand does not have it.
The CPython source for each item is linked in the item above (CPython v3.12.13).
AI disclosure: this report was drafted with Claude Code (claude-opus-5-5) and reviewed by me before submission. The divergences come from a differential-testing run against CPython 3.12.13, and every source reference was checked against the linked revisions.