Skip to content

Inconsisted behavior of ternary pow() for Python classes and C extension types #130104

Closed
@skirpichev

Description

@skirpichev

Bug report

Bug description:

Consider an example:

Python 3.13.1 (tags/v3.13.1:0671451779, Dec  4 2024, 07:55:26) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import example
>>> pow(42, example.xxx(), example.xxx())  # (1)
called power
123
>>> pow(42, example.xxx())
called power
123
>>> class A:
...     def __pow__(self, other, mod=None):
...         print("__pow__")
...         return 123
...     def __rpow__(self, other, mod=None):
...         print("__rpow__")
...         return 321
...         
>>> pow(42, A(), A())  # (2)
Traceback (most recent call last):
  File "<python-input-4>", line 1, in <module>
    pow(42, A(), A())
    ~~~^^^^^^^^^^^^^^
TypeError: unsupported operand type(s) for ** or pow(): 'int', 'A', 'A'
>>> pow(42, A())
__rpow__
321

In presence of the mod argument, C extension type (1) behave differently than the pure-Python analog (2). That looks as a bug. There is no documentation anywhere that explains how any of this is supposed to work so it’s hard to say that an alternative Python implementation like PyPy is doing anything incorrect or not.

Third-party extensions (like gmpy2) already uses (1) to do things like pow(int, gmpy2.mpz, gmpy2.mpz) - work. Unfortunately, it's not possible to implement a pure-Python class like gmpy2.mpz, that able to do this.

example.c
/* example.c */

#define PY_SSIZE_T_CLEAN
#include <Python.h>

PyTypeObject XXX_Type;

static PyObject *
new(PyTypeObject *type, PyObject *args, PyObject *keywds)
{
    return PyObject_New(PyObject, &XXX_Type);
}

static PyObject *
power(PyObject *self, PyObject *other, PyObject *module)
{
    printf("called power\n");
    return PyLong_FromLong(123);
}

static PyNumberMethods xxx_as_number = {
    .nb_power = power,
};

PyTypeObject XXX_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "xxx",
    .tp_new = new,
    .tp_as_number = &xxx_as_number,
};

static struct PyModuleDef ex_module = {
    PyModuleDef_HEAD_INIT,
    "example",
    "Test module.",
    -1,
    NULL,
};

PyMODINIT_FUNC
PyInit_example(void)
{
    PyObject *m = PyModule_Create(&ex_module);
    if (PyModule_AddType(m, &XXX_Type) < 0) {
        return -1;
    }
    return m;
}

PS: This was discussed before in https://discuss.python.org/t/35185.

CPython versions tested on:

CPython main branch

Operating systems tested on:

No response

Linked PRs

Metadata

Metadata

Labels

interpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or error

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions