feat(numpy): add NPY_HALF_ to npy_api::constants (#6151)

* feat(numpy): add NPY_HALF_ to npy_api::constants

* tests: round-trip a user-defined half dtype as numpy.float16
diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h
index 22e11bc..5b1161d 100644
--- a/include/pybind11/numpy.h
+++ b/include/pybind11/numpy.h
@@ -235,6 +235,7 @@
         NPY_STRING_,
         NPY_UNICODE_,
         NPY_VOID_,
+        NPY_HALF_ = 23, // NPY_DATETIME (21) and NPY_TIMEDELTA (22) are not mirrored
         // Platform-dependent normalization
         NPY_INT8_ = NPY_BYTE_,
         NPY_UINT8_ = NPY_UBYTE_,
diff --git a/tests/test_numpy_dtypes.cpp b/tests/test_numpy_dtypes.cpp
index d6d79e2..6844cc0 100644
--- a/tests/test_numpy_dtypes.cpp
+++ b/tests/test_numpy_dtypes.cpp
@@ -318,6 +318,21 @@
 struct A {};
 struct B {};
 
+struct UserHalf {
+    uint16_t bits;
+};
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+PYBIND11_NAMESPACE_BEGIN(detail)
+template <>
+struct npy_format_descriptor<UserHalf> {
+    static constexpr auto name = const_name("numpy.float16");
+    static constexpr int value = npy_api::NPY_HALF_;
+    static pybind11::dtype dtype() { return pybind11::dtype(/*typenum*/ value); }
+};
+PYBIND11_NAMESPACE_END(detail)
+PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
+
 TEST_SUBMODULE(numpy_dtypes, m) {
     try {
         py::module_::import("numpy");
@@ -646,6 +661,10 @@
     PYBIND11_NUMPY_DTYPE(TrailingPaddingStruct, a, b);
     m.def("trailing_padding_dtype", []() { return py::dtype::of<TrailingPaddingStruct>(); });
 
+    // test_half_dtype (issue #4061)
+    m.def("half_dtype_num", []() { return py::dtype::num_of<UserHalf>(); });
+    m.def("half_roundtrip", [](const py::array_t<UserHalf> &arr) { return arr; });
+
     // test_string_array
     m.def("create_string_array", [](bool non_empty) {
         py::array_t<StringStruct, 0> arr = mkarray_via_buffer<StringStruct>(non_empty ? 4 : 0);
diff --git a/tests/test_numpy_dtypes.py b/tests/test_numpy_dtypes.py
index 13a696c..2e5a67b 100644
--- a/tests/test_numpy_dtypes.py
+++ b/tests/test_numpy_dtypes.py
@@ -205,6 +205,13 @@
     assert (m.test_dtype_switch(arr.astype("longdouble")) == arr + 1).all()
 
 
+def test_half_dtype():
+    assert m.half_dtype_num() == np.dtype("float16").num
+
+    result = m.half_roundtrip(np.array([1.5, 2.25, -3.0], dtype=np.float16))
+    assert result.dtype == np.float16
+
+
 def test_templated_dtype():
     """A type spelled with a comma needs PYBIND11_TYPE here."""
     plain, renamed = m.templated_dtypes()