Skip to content

Assorted CPython compatibility divergences (7 items) #9748

Open

Description

@jseop-lim

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

  1. added
    C-compatA discrepancy between RustPython and CPython
    on Nov 8, 2026
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

    C-compatA discrepancy between RustPython and CPython

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions