Repository navigation
Description
Feature
Found 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.
| # | API | RustPython | CPython |
|---|---|---|---|
| 1 | tuple.__bool__ |
present | absent |
| 2 | weakref.ref(staticmethod(f)), classmethod(f) |
accepted | TypeError |
| 3 | 0 / -5 at runtime |
0.0 |
-0.0 |
| 4 | itertools.count(True) |
count(True) |
count(1) |
| 5 | unicodedata.normalize('X', '') |
ValueError |
'' |
| 6 | types.TracebackType(tb_next=...) with keywords |
TypeError |
traceback |
| 7 | pow(None, None, C()) |
AttributeError |
TypeError |
1. tuple has a __bool__ slot
class T(tuple):
def __len__(self):
return 0| 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__.
Fix suggestion
Remove impl AsNumber for PyTuple and its AsNumber entry in the pyclass(with(...)) list.
2. 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).
Fix suggestion
Drop HAS_WEAKREF from the two pyclass flags.
3. 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.
Fix suggestion
In true_div, return -0.0 when the numerator is zero and the denominator is negative.
4. 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.
Fix suggestion
In py_new, when start and step are int instances, step == 1 and start fits in isize, store start as an exact int.
5. 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).
Fix suggestion
Take form as a PyStrRef in normalize and is_normalized, return early for an empty unistr, and parse the form after that.
6. types.TracebackType() rejects keyword arguments
import 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).
Fix suggestion
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)].
7. Three-argument pow() calls __pow__ on the wrong operand when only the modulus defines it
class 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.
Fix suggestion
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.
Python Documentation or reference to CPython source code
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.
Activity