Skip to content

Commit 0a31d0e

Browse files
pytypes: Add doc section and tests about interaction with None
1 parent 1caf1d0 commit 0a31d0e

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

docs/advanced/pycpp/object.rst

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ Available types include :class:`handle`, :class:`object`, :class:`bool_`,
1313
:class:`iterable`, :class:`iterator`, :class:`function`, :class:`buffer`,
1414
:class:`array`, and :class:`array_t`.
1515

16+
.. warning::
17+
18+
You should be aware of how these classes interact with :func:`py::none`.
19+
See :ref:`pytypes_interaction_with_none` for more details.
20+
1621
Casting back and forth
1722
======================
1823

@@ -168,3 +173,62 @@ Generalized unpacking according to PEP448_ is also supported:
168173
Python functions from C++, including keywords arguments and unpacking.
169174

170175
.. _PEP448: https://www.python.org/dev/peps/pep-0448/
176+
177+
.. _pytypes_interaction_with_none:
178+
179+
Interaction with None
180+
=====================
181+
182+
You may be tempted to use types like ``py::str`` and ``py::dict`` in C++
183+
signatures (either pure C++, or in bound signatures). However, there are some
184+
"gotchas" for ``py::none()`` and how it interacts with these types. In best
185+
case scenarios, it will fail fast (e.g. with default arguments); in worst
186+
cases, it will silently work but corrupt the types you want to work with.
187+
188+
At a first glance, you may think after executing the following code, the
189+
expression ``my_value.is(py::none())`` will be true:
190+
191+
.. code-block:: cpp
192+
193+
py::str my_value = py::none();
194+
195+
However, this is not the case. Instead, the value of ``my_value`` will be equal
196+
to the Python value of ``str(None)``, due to how :macro:`PYBIND11_OBJECT_CVT`
197+
is used in :file:`pybind11/pytypes.h`.
198+
199+
Additionally, calling the following binding with the default argument used will
200+
raise a ``TypeError`` about invalid arguments:
201+
202+
.. code-block:: cpp
203+
204+
m.def(
205+
"my_function",
206+
[](py::str my_value) { ... },
207+
py::arg("my_value") = py::none());
208+
209+
In both of these cases where you may want to pass ``None`` through any
210+
signatures where you want to constrain the type, you should either use
211+
:class:`py::object` in conjunction with :func:`py::isinstance`, or use the
212+
corresponding C++ type with `std::optional` (if it is available on your
213+
system).
214+
215+
For the above conversion:
216+
217+
.. code-block:: cpp
218+
219+
py::object my_value = /* py::none() or some string */;
220+
...
221+
if (!my_value.is(py::none()) && !py::isinstance<py::str>(my_value)) {
222+
/* error behavior */
223+
}
224+
225+
For the above default argument:
226+
227+
.. code-block:: cpp
228+
229+
m.def(
230+
"my_function",
231+
[](std::optional<std::string> my_value) { ... },
232+
py::arg("my_value") = std::nullopt);
233+
234+
For more details, see the tests for ``pytypes`` mentioned above.

tests/test_pytypes.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,24 @@ TEST_SUBMODULE(pytypes, m) {
319319
return a[py::slice(0, -1, 2)];
320320
});
321321

322+
// See #2361
323+
// These four tests should reflect the text in `object.rst`, the
324+
// "Interaction with None" section.
325+
m.def("test_str_with_default_arg_none" ,[](py::str value) {
326+
return value;
327+
}, py::arg("value") = py::none());
328+
m.def("test_str_assign_none", []() {
329+
py::str is_this_none = py::none();
330+
return is_this_none;
331+
});
332+
m.def("test_dict_with_default_arg_none", [](py::dict value) {
333+
return value;
334+
}, py::arg("value") = py::none());
335+
m.def("test_dict_assign_none", []() {
336+
py::dict is_this_none = py::none();
337+
return is_this_none;
338+
});
339+
322340
m.def("test_memoryview_object", [](py::buffer b) {
323341
return py::memoryview(b);
324342
});

tests/test_pytypes.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,21 @@ def test_list_slicing():
281281
assert li[::2] == m.test_list_slicing(li)
282282

283283

284+
def test_pytypes_with_none():
285+
# See issue #2361
286+
assert m.test_str_assign_none() == "None"
287+
with pytest.raises(TypeError) as excinfo:
288+
m.test_str_with_default_arg_none()
289+
assert "incompatible function arguments" in str(excinfo.value)
290+
291+
with pytest.raises(TypeError) as excinfo:
292+
assert m.test_dict_assign_none()
293+
assert "'NoneType' object is not iterable" in str(excinfo.value)
294+
with pytest.raises(TypeError) as excinfo:
295+
m.test_dict_with_default_arg_none()
296+
assert "incompatible function arguments" in str(excinfo.value)
297+
298+
284299
@pytest.mark.parametrize('method, args, fmt, expected_view', [
285300
(m.test_memoryview_object, (b'red',), 'B', b'red'),
286301
(m.test_memoryview_buffer_info, (b'green',), 'B', b'green'),

0 commit comments

Comments
 (0)