Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
515a3ab
init
InvincibleRMC Oct 23, 2025
aa2693f
Add constexpr to is_floating_point check
gentlegiantJGC Oct 23, 2025
7948b27
Allow noconvert float to accept int
gentlegiantJGC Oct 23, 2025
47746af
Update noconvert documentation
gentlegiantJGC Oct 23, 2025
507d31c
Allow noconvert complex to accept int and float
gentlegiantJGC Oct 23, 2025
f4115fe
Merge remote-tracking branch 'collab/fix-pep-484' into expand-float-s…
InvincibleRMC Oct 23, 2025
4ea8bcb
Add complex strict test
InvincibleRMC Oct 23, 2025
81e49f6
style: pre-commit fixes
pre-commit-ci[bot] Oct 23, 2025
0b94f21
Update unit tests so int, becomes double.
InvincibleRMC Oct 23, 2025
e9bf4e5
style: pre-commit fixes
pre-commit-ci[bot] Oct 23, 2025
21f8447
remove if (constexpr)
InvincibleRMC Oct 23, 2025
5806318
fix spelling error
InvincibleRMC Oct 23, 2025
a0fb6dc
bump order in #else
InvincibleRMC Oct 23, 2025
2692820
Switch order in c++11 only section
InvincibleRMC Oct 24, 2025
b12f5a8
ci: trigger build
InvincibleRMC Oct 24, 2025
2187a65
ci: trigger build
InvincibleRMC Oct 24, 2025
d42c8e8
Allow casting from float to int
gentlegiantJGC Oct 24, 2025
16bdff3
tests for py::float into int
InvincibleRMC Oct 24, 2025
358266f
Update complex_cast tests
InvincibleRMC Oct 24, 2025
dadbf05
Add SupportsIndex to int and float
InvincibleRMC Oct 24, 2025
248b12e
style: pre-commit fixes
pre-commit-ci[bot] Oct 24, 2025
2163f50
fix assert
InvincibleRMC Oct 24, 2025
c13f21e
Update docs to mention other conversions
InvincibleRMC Oct 24, 2025
f4ed7b7
fix pypy __index__ problems
InvincibleRMC Oct 24, 2025
fc815ec
style: pre-commit fixes
pre-commit-ci[bot] Oct 24, 2025
388366f
extract out PyLong_AsLong __index__ deprecation
InvincibleRMC Oct 26, 2025
a956052
style: pre-commit fixes
pre-commit-ci[bot] Oct 26, 2025
962c9fa
Add back env.deprecated_call
InvincibleRMC Oct 26, 2025
7b1bc72
remove note
InvincibleRMC Oct 26, 2025
edbcbf2
remove untrue comment
InvincibleRMC Oct 26, 2025
a3a4a5e
fix noconvert_args
InvincibleRMC Oct 26, 2025
d5dab14
resolve error
InvincibleRMC Oct 26, 2025
83ade19
Add comment
InvincibleRMC Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions docs/advanced/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -437,10 +437,10 @@ Certain argument types may support conversion from one type to another. Some
examples of conversions are:

* :ref:`implicit_conversions` declared using ``py::implicitly_convertible<A,B>()``
* Calling a method accepting a double with an integer argument
* Calling a ``std::complex<float>`` argument with a non-complex python type
(for example, with a float). (Requires the optional ``pybind11/complex.h``
header).
* Passing an argument that implements ``__float__`` or ``__index__`` to ``float`` or ``double``.
* Passing an argument that implements ``__int__`` or ``__index__`` to ``int``.
* Passing an argument that implements ``__complex__``, ``__float__``, or ``__index__`` to ``std::complex<float>``.
(Requires the optional ``pybind11/complex.h`` header).
* Calling a function taking an Eigen matrix reference with a numpy array of the
wrong type or of an incompatible data layout. (Requires the optional
``pybind11/eigen.h`` header).
Expand All @@ -452,24 +452,37 @@ object, such as:

.. code-block:: cpp

m.def("floats_only", [](double f) { return 0.5 * f; }, py::arg("f").noconvert());
m.def("floats_preferred", [](double f) { return 0.5 * f; }, py::arg("f"));
m.def("supports_float", [](double f) { return 0.5 * f; }, py::arg("f"));
m.def("only_float", [](double f) { return 0.5 * f; }, py::arg("f").noconvert());

Attempting the call the second function (the one without ``.noconvert()``) with
an integer will succeed, but attempting to call the ``.noconvert()`` version
will fail with a ``TypeError``:
``supports_float`` will accept any argument that implements ``__float__`` or ``__index__``.
``only_float`` will only accept a float or int argument. Anything else will fail with a ``TypeError``:

.. note::

The noconvert behaviour of float, double and complex has changed to match PEP 484.
A float/double argument marked noconvert will accept float or int.
A std::complex<float> argument will accept complex, float or int.

.. code-block:: pycon

>>> floats_preferred(4)
class MyFloat:
def __init__(self, value: float) -> None:
self._value = float(value)
def __repr__(self) -> str:
return f"MyFloat({self._value})"
def __float__(self) -> float:
return self._value

>>> supports_float(MyFloat(4))
2.0
>>> floats_only(4)
>>> only_float(MyFloat(4))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: floats_only(): incompatible function arguments. The following argument types are supported:
TypeError: only_float(): incompatible function arguments. The following argument types are supported:
1. (f: float) -> float

Invoked with: 4
Invoked with: MyFloat(4)

You may, of course, combine this with the :var:`_a` shorthand notation (see
:ref:`keyword_args`) and/or :ref:`default_args`. It is also permitted to omit
Expand Down
28 changes: 11 additions & 17 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -244,29 +244,18 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value && !is_std_char_t
return false;
}

#if !defined(PYPY_VERSION)
auto index_check = [](PyObject *o) { return PyIndex_Check(o); };
#else
// In PyPy 7.3.3, `PyIndex_Check` is implemented by calling `__index__`,
// while CPython only considers the existence of `nb_index`/`__index__`.
auto index_check = [](PyObject *o) { return hasattr(o, "__index__"); };
#endif

if (std::is_floating_point<T>::value) {
if (convert || PyFloat_Check(src.ptr())) {
if (convert || PyFloat_Check(src.ptr()) || PYBIND11_LONG_CHECK(src.ptr())) {
py_value = (py_type) PyFloat_AsDouble(src.ptr());
} else {
return false;
}
} else if (PyFloat_Check(src.ptr())
|| (!convert && !PYBIND11_LONG_CHECK(src.ptr()) && !index_check(src.ptr()))) {
return false;
} else {
} else if (convert || PYBIND11_LONG_CHECK(src.ptr()) || PYBIND11_INDEX_CHECK(src.ptr())) {
handle src_or_index = src;
// PyPy: 7.3.7's 3.8 does not implement PyLong_*'s __index__ calls.
#if defined(PYPY_VERSION)
object index;
if (!PYBIND11_LONG_CHECK(src.ptr())) { // So: index_check(src.ptr())
if (!PYBIND11_LONG_CHECK(src.ptr())) { // So: PYBIND11_INDEX_CHECK(src.ptr())
index = reinterpret_steal<object>(PyNumber_Index(src.ptr()));
if (!index) {
PyErr_Clear();
Expand All @@ -284,6 +273,8 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value && !is_std_char_t
? (py_type) PyLong_AsLong(src_or_index.ptr())
: (py_type) PYBIND11_LONG_AS_LONGLONG(src_or_index.ptr());
}
} else {
return false;
}

// Python API reported an error
Expand Down Expand Up @@ -347,9 +338,12 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value && !is_std_char_t
return PyLong_FromUnsignedLongLong((unsigned long long) src);
}

PYBIND11_TYPE_CASTER(T,
io_name<std::is_integral<T>::value>(
"typing.SupportsInt", "int", "typing.SupportsFloat", "float"));
PYBIND11_TYPE_CASTER(
T,
io_name<std::is_integral<T>::value>("typing.SupportsInt | typing.SupportsIndex",
"int",
"typing.SupportsFloat | typing.SupportsIndex",
"float"));
};

template <typename T>
Expand Down
25 changes: 22 additions & 3 deletions include/pybind11/complex.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,26 @@ class type_caster<std::complex<T>> {
if (!src) {
return false;
}
if (!convert && !PyComplex_Check(src.ptr())) {
if (!convert
&& !(PyComplex_Check(src.ptr()) || PyFloat_Check(src.ptr())
|| PYBIND11_LONG_CHECK(src.ptr()))) {
return false;
}
Py_complex result = PyComplex_AsCComplex(src.ptr());
handle src_or_index = src;
#if defined(PYPY_VERSION)
object index;
if (PYBIND11_INDEX_CHECK(src.ptr())) {
index = reinterpret_steal<object>(PyNumber_Index(src.ptr()));
if (!index) {
PyErr_Clear();
if (!convert)
return false;
} else {
src_or_index = index;
}
}
#endif
Py_complex result = PyComplex_AsCComplex(src_or_index.ptr());
if (result.real == -1.0 && PyErr_Occurred()) {
PyErr_Clear();
return false;
Expand All @@ -68,7 +84,10 @@ class type_caster<std::complex<T>> {
return PyComplex_FromDoubles((double) src.real(), (double) src.imag());
}

PYBIND11_TYPE_CASTER(std::complex<T>, const_name("complex"));
PYBIND11_TYPE_CASTER(
std::complex<T>,
io_name("typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex",
"complex"));
};
PYBIND11_NAMESPACE_END(detail)
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
7 changes: 7 additions & 0 deletions include/pybind11/detail/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,13 @@
#define PYBIND11_BYTES_AS_STRING PyBytes_AsString
#define PYBIND11_BYTES_SIZE PyBytes_Size
#define PYBIND11_LONG_CHECK(o) PyLong_Check(o)
// In PyPy 7.3.3, `PyIndex_Check` is implemented by calling `__index__`,
// while CPython only considers the existence of `nb_index`/`__index__`.
#if !defined(PYPY_VERSION)
# define PYBIND11_INDEX_CHECK(o) PyIndex_Check(o)
#else
# define PYBIND11_INDEX_CHECK(o) hasattr(o, "__index__")
#endif
#define PYBIND11_LONG_AS_LONGLONG(o) PyLong_AsLongLong(o)
#define PYBIND11_LONG_FROM_SIGNED(o) PyLong_FromSsize_t((ssize_t) (o))
#define PYBIND11_LONG_FROM_UNSIGNED(o) PyLong_FromSize_t((size_t) (o))
Expand Down
7 changes: 7 additions & 0 deletions tests/test_builtin_casters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,13 @@ TEST_SUBMODULE(builtin_casters, m) {
m.def("complex_cast", [](float x) { return "{}"_s.format(x); });
m.def("complex_cast",
[](std::complex<float> x) { return "({}, {})"_s.format(x.real(), x.imag()); });
m.def(
"complex_cast_strict",
[](std::complex<float> x) { return "({}, {})"_s.format(x.real(), x.imag()); },
py::arg{}.noconvert());

m.def("complex_convert", [](std::complex<float> x) { return x; });
m.def("complex_noconvert", [](std::complex<float> x) { return x; }, py::arg{}.noconvert());

// test int vs. long (Python 2)
m.def("int_cast", []() { return (int) 42; });
Expand Down
90 changes: 85 additions & 5 deletions tests/test_builtin_casters.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,10 @@

convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert

assert doc(convert) == "int_passthrough(arg0: typing.SupportsInt) -> int"
assert (
doc(convert)
== "int_passthrough(arg0: typing.SupportsInt | typing.SupportsIndex) -> int"
)
assert doc(noconvert) == "int_passthrough_noconvert(arg0: int) -> int"

def requires_conversion(v):
Expand All @@ -297,7 +300,8 @@

assert convert(7) == 7
assert noconvert(7) == 7
cant_convert(3.14159)
assert convert(3.14159) == 3

Check warning on line 303 in tests/test_builtin_casters.py

View workflow job for this annotation

GitHub Actions / 🐍 (ubuntu-22.04, 3.8, -DPYBIND11_FINDPYTHON=OFF -DPYBIND11_NUMPY_1_ONLY=ON) / 🧪

an integer is required (got type float). Implicit conversion to integers using __int__ is deprecated, and may be removed in a future version of Python.

Check warning on line 303 in tests/test_builtin_casters.py

View workflow job for this annotation

GitHub Actions / 🐍 (ubuntu-latest, 3.8, -DPYBIND11_FINDPYTHON=ON -DCMAKE_CXX_STANDARD=17) / 🧪

an integer is required (got type float). Implicit conversion to integers using __int__ is deprecated, and may be removed in a future version of Python.
requires_conversion(3.14159)
# TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar)
# TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7)
if sys.version_info < (3, 10) and env.CPYTHON:
Expand All @@ -312,6 +316,7 @@
# Before Python 3.8, `PyLong_AsLong` does not pick up on `obj.__index__`,
# but pybind11 "backports" this behavior.
assert convert(Index()) == 42
assert isinstance(convert(Index()), int)
assert noconvert(Index()) == 42
assert convert(IntAndIndex()) == 0 # Fishy; `int(DoubleThought)` == 42
assert noconvert(IntAndIndex()) == 0
Expand All @@ -322,19 +327,39 @@


def test_float_convert(doc):
class Int:
def __int__(self):
return -5

class Index:
def __index__(self) -> int:
return -7

class Float:
def __float__(self):
return 41.45

convert, noconvert = m.float_passthrough, m.float_passthrough_noconvert
assert doc(convert) == "float_passthrough(arg0: typing.SupportsFloat) -> float"
assert (
doc(convert)
== "float_passthrough(arg0: typing.SupportsFloat | typing.SupportsIndex) -> float"
)
assert doc(noconvert) == "float_passthrough_noconvert(arg0: float) -> float"

def requires_conversion(v):
pytest.raises(TypeError, noconvert, v)

def cant_convert(v):
pytest.raises(TypeError, convert, v)

requires_conversion(Float())
requires_conversion(Index())
assert pytest.approx(convert(Float())) == 41.45
assert pytest.approx(convert(Index())) == -7.0
assert isinstance(convert(Float()), float)
assert pytest.approx(convert(3)) == 3.0
assert pytest.approx(noconvert(3)) == 3.0
cant_convert(Int())


def test_numpy_int_convert():
Expand Down Expand Up @@ -381,7 +406,7 @@
assert (
doc(m.tuple_passthrough)
== """
tuple_passthrough(arg0: tuple[bool, str, typing.SupportsInt]) -> tuple[int, str, bool]
tuple_passthrough(arg0: tuple[bool, str, typing.SupportsInt | typing.SupportsIndex]) -> tuple[int, str, bool]
Return a triple in reversed order
"""
Expand Down Expand Up @@ -458,11 +483,66 @@
assert m.refwrap_call_iiw(IncType(10), m.refwrap_iiw) == [10, 10, 10, 10]


def test_complex_cast():
def test_complex_cast(doc):
"""std::complex casts"""

class Complex:
def __complex__(self) -> complex:
return complex(5, 4)

class Float:
def __float__(self) -> float:
return 5.0

class Int:
def __int__(self) -> int:
return 3

class Index:
def __index__(self) -> int:
return 1

assert m.complex_cast(1) == "1.0"
assert m.complex_cast(1.0) == "1.0"
assert m.complex_cast(Complex()) == "(5.0, 4.0)"
assert m.complex_cast(2j) == "(0.0, 2.0)"

assert m.complex_cast_strict(1) == "(1.0, 0.0)"
assert m.complex_cast_strict(3.0) == "(3.0, 0.0)"
assert m.complex_cast_strict(complex(5, 4)) == "(5.0, 4.0)"
assert m.complex_cast_strict(2j) == "(0.0, 2.0)"

convert, noconvert = m.complex_convert, m.complex_noconvert

def requires_conversion(v):
pytest.raises(TypeError, noconvert, v)

def cant_convert(v):
pytest.raises(TypeError, convert, v)

assert (
doc(convert)
== "complex_convert(arg0: typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex) -> complex"
)
assert doc(noconvert) == "complex_noconvert(arg0: complex) -> complex"

assert convert(1) == 1.0
assert convert(2.0) == 2.0
assert convert(1 + 5j) == 1.0 + 5.0j
assert convert(Complex()) == 5.0 + 4j
assert convert(Float()) == 5.0
assert isinstance(convert(Float()), complex)
cant_convert(Int())
assert convert(Index()) == 1
assert isinstance(convert(Index()), complex)

assert noconvert(1) == 1.0
assert noconvert(2.0) == 2.0
assert noconvert(1 + 5j) == 1.0 + 5.0j
requires_conversion(Complex())
requires_conversion(Float())
requires_conversion(Index())


def test_bool_caster():
"""Test bool caster implicit conversions."""
Expand Down
4 changes: 2 additions & 2 deletions tests/test_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,11 @@ def test_cpp_function_roundtrip():
def test_function_signatures(doc):
assert (
doc(m.test_callback3)
== "test_callback3(arg0: collections.abc.Callable[[typing.SupportsInt], int]) -> str"
== "test_callback3(arg0: collections.abc.Callable[[typing.SupportsInt | typing.SupportsIndex], int]) -> str"
)
assert (
doc(m.test_callback4)
== "test_callback4() -> collections.abc.Callable[[typing.SupportsInt], int]"
== "test_callback4() -> collections.abc.Callable[[typing.SupportsInt | typing.SupportsIndex], int]"
)


Expand Down
4 changes: 2 additions & 2 deletions tests/test_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,13 @@ def test_qualname(doc):
assert (
doc(m.NestBase.Nested.fn)
== """
fn(self: m.class_.NestBase.Nested, arg0: typing.SupportsInt, arg1: m.class_.NestBase, arg2: m.class_.NestBase.Nested) -> None
fn(self: m.class_.NestBase.Nested, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: m.class_.NestBase, arg2: m.class_.NestBase.Nested) -> None
"""
)
assert (
doc(m.NestBase.Nested.fa)
== """
fa(self: m.class_.NestBase.Nested, a: typing.SupportsInt, b: m.class_.NestBase, c: m.class_.NestBase.Nested) -> None
fa(self: m.class_.NestBase.Nested, a: typing.SupportsInt | typing.SupportsIndex, b: m.class_.NestBase, c: m.class_.NestBase.Nested) -> None
"""
)
assert m.NestBase.__module__ == "pybind11_tests.class_"
Expand Down
Loading
Loading