Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 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
a2df4eb
[skip ci]
rwgk Nov 8, 2025
754e2d0
Add test to verify that custom __index__ objects (not PyLong) work co…
rwgk Nov 8, 2025
b383b06
Merge master into expand-float-strict
rwgk Nov 11, 2025
41ff876
Improve comment clarity for PyPy __index__ handling
rwgk Nov 11, 2025
0a00147
Undo inconsequential change to regex in test_enum.py
rwgk Nov 11, 2025
c7e959e
test_methods_and_attributes.py: Restore existing `m.overload_order(1.…
rwgk Nov 11, 2025
e4023cd
Reject float → int conversion even in convert mode
rwgk Nov 12, 2025
cc981a8
Revert test changes that sidestepped implicit float→int conversion
rwgk Nov 12, 2025
cc411c6
Replace env.deprecated_call() with pytest.deprecated_call()
rwgk Nov 12, 2025
ac957e3
Update test expectations for swapped NoisyAlloc overloads
rwgk Nov 12, 2025
32fc2e8
Resolve clang-tidy error:
rwgk Nov 12, 2025
2a32770
Merge branch 'master' into InvincibleRMC→expand-float-strict
rwgk Nov 12, 2025
f1d8158
Add test coverage for __index__ and __int__ edge cases: incorrectly r…
rwgk Nov 12, 2025
0f1f8ea
Minor comment-only changes: add PR number, for easy future reference
rwgk Nov 12, 2025
816298a
Ensure we are not leaking a Python error is something is wrong elsewh…
rwgk Nov 12, 2025
cc3209c
Merge branch 'master' into expand-float-strict
InvincibleRMC Nov 14, 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
27 changes: 14 additions & 13 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -244,29 +244,28 @@ 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()))) {
|| !(convert || PYBIND11_LONG_CHECK(src.ptr())
|| PYBIND11_INDEX_CHECK(src.ptr()))) {
// Explicitly reject float → int conversion even in convert mode.
// This prevents silent truncation (e.g., 1.9 → 1).
// Only int → float conversion is allowed (widening, no precision loss).
// Also reject if none of the conversion conditions are met.
return false;
} else {
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 not a PyLong, we need to call PyNumber_Index explicitly on PyPy.
// When convert is false, we only reach here if PYBIND11_INDEX_CHECK passed above.
if (!PYBIND11_LONG_CHECK(src.ptr())) {
index = reinterpret_steal<object>(PyNumber_Index(src.ptr()));
if (!index) {
PyErr_Clear();
Expand All @@ -286,8 +285,10 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value && !is_std_char_t
}
}

// Python API reported an error
bool py_err = py_value == (py_type) -1 && PyErr_Occurred();
bool py_err = (PyErr_Occurred() != nullptr);
if (py_err) {
assert(py_value == static_cast<py_type>(-1));
}

// Check to see if the conversion is valid (integers should match exactly)
// Signed/unsigned checks happen elsewhere
Expand Down
4 changes: 3 additions & 1 deletion include/pybind11/complex.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ 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;
}
handle src_or_index = src;
Expand Down
25 changes: 25 additions & 0 deletions tests/test_builtin_casters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -363,9 +363,34 @@ 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_overload_resolution_float_int
// Test that float overload registered before int overload gets selected when passing int
// This documents the breaking change: int can now match float in strict mode
m.def("overload_resolution_test", [](float x) { return "float: " + std::to_string(x); });
m.def("overload_resolution_test", [](int x) { return "int: " + std::to_string(x); });

// Test with noconvert (strict mode) - this is the key breaking change
m.def(
"overload_resolution_strict",
[](float x) { return "float_strict: " + std::to_string(x); },
py::arg{}.noconvert());
m.def("overload_resolution_strict", [](int x) { return "int_strict: " + std::to_string(x); });

// Test complex overload resolution: complex registered before float/int
m.def("overload_resolution_complex", [](std::complex<float> x) {
return "complex: (" + std::to_string(x.real()) + ", " + std::to_string(x.imag()) + ")";
});
m.def("overload_resolution_complex", [](float x) { return "float: " + std::to_string(x); });
m.def("overload_resolution_complex", [](int x) { return "int: " + std::to_string(x); });

// test int vs. long (Python 2)
m.def("int_cast", []() { return (int) 42; });
m.def("long_cast", []() { return (long) 42; });
Expand Down
169 changes: 166 additions & 3 deletions tests/test_builtin_casters.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ def cant_convert(v):
# 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 @@ -323,6 +324,50 @@ def cant_convert(v):
assert convert(RaisingValueErrorOnIndex()) == 42
requires_conversion(RaisingValueErrorOnIndex())

class IndexReturnsFloat:
def __index__(self):
return 3.14 # noqa: PLE0305 Wrong: should return int

class IntReturnsFloat:
def __int__(self):
return 3.14 # Wrong: should return int

class IndexFloatIntInt:
def __index__(self):
return 3.14 # noqa: PLE0305 Wrong: should return int

def __int__(self):
return 42 # Correct: returns int

class IndexIntIntFloat:
def __index__(self):
return 42 # Correct: returns int

def __int__(self):
return 3.14 # Wrong: should return int

class IndexFloatIntFloat:
def __index__(self):
return 3.14 # noqa: PLE0305 Wrong: should return int

def __int__(self):
return 2.71 # Wrong: should return int

cant_convert(IndexReturnsFloat())
requires_conversion(IndexReturnsFloat())

cant_convert(IntReturnsFloat())
requires_conversion(IntReturnsFloat())

assert convert(IndexFloatIntInt()) == 42 # convert: __index__ fails, uses __int__
requires_conversion(IndexFloatIntInt()) # noconvert: __index__ fails, no fallback

assert convert(IndexIntIntFloat()) == 42 # convert: __index__ succeeds
assert noconvert(IndexIntIntFloat()) == 42 # noconvert: __index__ succeeds

cant_convert(IndexFloatIntFloat()) # convert mode rejects (both fail)
requires_conversion(IndexFloatIntFloat()) # noconvert mode also rejects


def test_float_convert(doc):
class Int:
Expand Down Expand Up @@ -356,7 +401,7 @@ def cant_convert(v):
assert pytest.approx(convert(Index())) == -7.0
assert isinstance(convert(Float()), float)
assert pytest.approx(convert(3)) == 3.0
requires_conversion(3)
assert pytest.approx(noconvert(3)) == 3.0
cant_convert(Int())


Expand Down Expand Up @@ -505,6 +550,11 @@ def __index__(self) -> int:
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):
Expand All @@ -529,14 +579,127 @@ def cant_convert(v):
assert convert(Index()) == 1
assert isinstance(convert(Index()), complex)

requires_conversion(1)
requires_conversion(2.0)
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_complex_index_handling():
"""
Test __index__ handling in complex caster (added with PR #5879).

This test verifies that custom __index__ objects (not PyLong) work correctly
with complex conversion. The behavior should be consistent across CPython,
PyPy, and GraalPy.

- Custom __index__ objects work with convert (non-strict mode)
- Custom __index__ objects do NOT work with noconvert (strict mode)
- Regular int (PyLong) works with both convert and noconvert
"""

class CustomIndex:
"""Custom class with __index__ but not __int__ or __float__"""

def __index__(self) -> int:
return 42

class CustomIndexNegative:
"""Custom class with negative __index__"""

def __index__(self) -> int:
return -17

convert, noconvert = m.complex_convert, m.complex_noconvert

# Test that regular int (PyLong) works
assert convert(5) == 5.0 + 0j
assert noconvert(5) == 5.0 + 0j

# Test that custom __index__ objects work with convert (non-strict mode)
# This exercises the PyPy-specific path in complex.h
assert convert(CustomIndex()) == 42.0 + 0j
assert convert(CustomIndexNegative()) == -17.0 + 0j

# With noconvert (strict mode), custom __index__ objects are NOT accepted
# Strict mode only accepts complex, float, or int (PyLong), not custom __index__ objects
def requires_conversion(v):
pytest.raises(TypeError, noconvert, v)

requires_conversion(CustomIndex())
requires_conversion(CustomIndexNegative())

# Verify the result is actually a complex
result = convert(CustomIndex())
assert isinstance(result, complex)
assert result.real == 42.0
assert result.imag == 0.0


def test_overload_resolution_float_int():
"""
Test overload resolution behavior when int can match float (added with PR #5879).

This test documents the breaking change in PR #5879: when a float overload is
registered before an int overload, passing a Python int will now match the float
overload (because int can be converted to float in strict mode per PEP 484).

Before PR #5879: int(42) would match int overload (if both existed)
After PR #5879: int(42) matches float overload (if registered first)

This is a breaking change because existing code that relied on int matching
int overloads may now match float overloads instead.
"""
# Test 1: float overload registered first, int second
# When passing int(42), pybind11 tries overloads in order:
# 1. float overload - can int(42) be converted? Yes (with PR #5879 changes)
# 2. Match! Use float overload (int overload never checked)
result = m.overload_resolution_test(42)
assert result == "float: 42.000000", (
f"Expected int(42) to match float overload, got: {result}. "
"This documents the breaking change: int now matches float overloads."
)
assert m.overload_resolution_test(42.0) == "float: 42.000000"

# Test 2: With noconvert (strict mode) - this is the KEY breaking change
# Before PR #5879: int(42) would NOT match float overload with noconvert, would match int overload
# After PR #5879: int(42) DOES match float overload with noconvert (because int->float is now allowed)
result_strict = m.overload_resolution_strict(42)
assert result_strict == "float_strict: 42.000000", (
f"Expected int(42) to match float overload with noconvert, got: {result_strict}. "
"This is the key breaking change: int now matches float even in strict mode."
)
assert m.overload_resolution_strict(42.0) == "float_strict: 42.000000"

# Test 3: complex overload registered first, then float, then int
# When passing int(5), pybind11 tries overloads in order:
# 1. complex overload - can int(5) be converted? Yes (with PR #5879 changes)
# 2. Match! Use complex overload
assert m.overload_resolution_complex(5) == "complex: (5.000000, 0.000000)"
assert m.overload_resolution_complex(5.0) == "complex: (5.000000, 0.000000)"
assert (
m.overload_resolution_complex(complex(3, 4)) == "complex: (3.000000, 4.000000)"
)

# Verify that the overloads are registered in the expected order
# The docstring should show float overload before int overload
doc = m.overload_resolution_test.__doc__
assert doc is not None
# Check that float overload appears before int overload in docstring
# The docstring uses "typing.SupportsFloat" and "typing.SupportsInt"
float_pos = doc.find("SupportsFloat")
int_pos = doc.find("SupportsInt")
assert float_pos != -1, f"Could not find 'SupportsFloat' in docstring: {doc}"
assert int_pos != -1, f"Could not find 'SupportsInt' in docstring: {doc}"
assert float_pos < int_pos, (
f"Float overload should appear before int overload in docstring. "
f"Found 'SupportsFloat' at {float_pos}, 'SupportsInt' at {int_pos}. "
f"Docstring: {doc}"
)


def test_bool_caster():
"""Test bool caster implicit conversions."""
convert, noconvert = m.bool_passthrough, m.bool_passthrough_noconvert
Expand Down
12 changes: 1 addition & 11 deletions tests/test_custom_type_casters.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,7 @@ def test_noconvert_args(msg):

assert m.floats_preferred(4) == 2.0
assert m.floats_only(4.0) == 2.0
with pytest.raises(TypeError) as excinfo:
m.floats_only(4)
assert (
msg(excinfo.value)
== """
floats_only(): incompatible function arguments. The following argument types are supported:
1. (f: float) -> float

Invoked with: 4
"""
)
assert m.floats_only(4) == 2.0

assert m.ints_preferred(4) == 2
assert m.ints_preferred(True) == 0
Expand Down
Loading
Loading