Repository: pybind/pybind11
Pull request #5555: feat: enable conversions between native Python enum types and C++ enums
Author: login: rwgk
Created: 2025-03-08T21:36:08Z | Merged: 2025-03-25T03:31:59Z

<!--
Title (above): please place [branch_name] at the beginning if you are targeting a branch other than master. *Do not target stable*.
It is recommended to use conventional commit format, see conventionalcommits.org, but not required.
-->
## Description
**This PR requires a `PYBIND11_INTERNALS_VERSION` bump (from `7` to `8`).**
________
For easy access during the review: [Updated documentation](https://pybind11--5555.org.readthedocs.build/en/5555/classes.html#enumerations-and-internal-types)
________

Alternative to `py::enum_`, binding C++ `enum` types to native Python enum types, typically types in Python's [stdlib enum](https://docs.python.org/3/library/enum.html) module. For example:
 
```C++
#include <pybind11/pybind11.h>
#include <pybind11/native_enum.h> // Not already included with pybind11.h

enum class altitude : char {
    high = 'h',
    low = 'l'
};

PYBIND11_MODULE(native_enum_demo, m) {
    py::native_enum<altitude>(m, "altitude")
             .value("high", altitude::high)
             .value("low", altitude::low)
             .finalize();
}
```

The `.finalize()` call is needed because Python's native enums cannot be built incrementally — all name/values need to be passed at once. To achieve this, `py::native_enum` acts as a buffer to collect the name/value pairs. The `.finalize()` call uses the accumulated name/value pairs to build the arguments for constructing a native Python enum type.

The established `py::enum_` is not [PEP 435](https://peps.python.org/pep-0435/) compatible (see also [#2332](https://github.com/pybind/pybind11/issues/2332)). `py::native_enum` provides an alternative without breaking backwards compatibility. Ideally, existing uses of `py::enum_` should migrate to `py::native_enum`, but `py::enum_` will remain supported indefinitely.

In rare cases it may be necessary to specialize `pybind11::detail::type_caster_enum_type_enabled`:

```C++
#if defined(PYBIND11_HAS_NATIVE_ENUM)                                   
namespace pybind11::detail {                                            
template <typename FancyEnum>                                           
struct type_caster_enum_type_enabled<                                   
    FancyEnum,                                                          
    std::enable_if_t<is_fancy_enum<FancyEnum>::value>> : std::false_type {};
}                                                                       
#endif                                                                  
```

This specialization is needed only if the custom type caster is templated.

The `PYBIND11_HAS_NATIVE_ENUM` guard is needed only if backward compatibility with pybind11v2 is required.
________
This PR sidesteps issues discovered under pybind/pybind11#2744.

This PR fixes https://github.com/pybind/pybind11/pull/3643#issuecomment-1022987818, more-or-less as a side-effect.
________

`cloc --diff` results for `include/` between current master and this PR:
```
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
C/C++ Header
 same                           39           1816           3347          16428
 modified                        5              0              0              6
 added                           2             53             18            332
 removed                         0              1              0              7
```
I.e., this PR increases the production code size by 2.0%.
________

This PR is based on google/pybind11clif#30005 (including accumulated follow-on fixes) and google/pybind11clif#30140 (remove_cross_extension_shared_state branch).

Until August 2024, this code was included in PyCLIF-pybind11 global testing in the Google environment, which provided thousands of diverse use cases that needed to satisfy many million unit tests (the PyCLIF-pybind11 success was about 99.999%).

<!-- Include relevant issues or PRs here, describe what changed and why -->


## Suggested changelog entry:

<!-- Fill in the below block with the expected RestructuredText entry. Delete if no entry needed;
     but do not delete header or rst block if an entry is needed! Will be collected via a script. -->

```rst
``py::native_enum`` was added, for conversions between Python's native (stdlib) enum types and C++ enums.
```

<!-- If the upgrade guide needs updating, note that here too -->



Diff:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 23e114e20a..2161c92693 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -136,6 +136,7 @@ set(PYBIND11_HEADERS
     include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h
     include/pybind11/detail/init.h
     include/pybind11/detail/internals.h
+    include/pybind11/detail/native_enum_data.h
     include/pybind11/detail/struct_smart_holder.h
     include/pybind11/detail/type_caster_base.h
     include/pybind11/detail/typeid.h
@@ -162,6 +163,7 @@ set(PYBIND11_HEADERS
     include/pybind11/gil_safe_call_once.h
     include/pybind11/iostream.h
     include/pybind11/functional.h
+    include/pybind11/native_enum.h
     include/pybind11/numpy.h
     include/pybind11/operators.h
     include/pybind11/pybind11.h
diff --git a/docs/advanced/cast/custom.rst b/docs/advanced/cast/custom.rst
index 5a626f3ba8..1de0f0a21a 100644
--- a/docs/advanced/cast/custom.rst
+++ b/docs/advanced/cast/custom.rst
@@ -1,3 +1,5 @@
+.. _custom_type_caster:
+
 Custom type casters
 ===================
 
diff --git a/docs/advanced/deprecated.rst b/docs/advanced/deprecated.rst
new file mode 100644
index 0000000000..a43c447aa5
--- /dev/null
+++ b/docs/advanced/deprecated.rst
@@ -0,0 +1,113 @@
+Deprecated
+##########
+
+.. _deprecated_enum:
+
+``py::enum_``
+=============
+
+This is the original documentation for ``py::enum_``, which is deprecated
+because it is not `PEP 435 compatible <https://peps.python.org/pep-0435/>`_
+(see also `#2332 <https://github.com/pybind/pybind11/issues/2332>`_).
+Please prefer ``py::native_enum`` (added with pybind11v3) when writing
+new bindings. See :ref:`native_enum` for more information.
+
+Let's suppose that we have an example class that contains internal types
+like enumerations, e.g.:
+
+.. code-block:: cpp
+
+    struct Pet {
+        enum Kind {
+            Dog = 0,
+            Cat
+        };
+
+        struct Attributes {
+            float age = 0;
+        };
+
+        Pet(const std::string &name, Kind type) : name(name), type(type) { }
+
+        std::string name;
+        Kind type;
+        Attributes attr;
+    };
+
+The binding code for this example looks as follows:
+
+.. code-block:: cpp
+
+    py::class_<Pet> pet(m, "Pet");
+
+    pet.def(py::init<const std::string &, Pet::Kind>())
+        .def_readwrite("name", &Pet::name)
+        .def_readwrite("type", &Pet::type)
+        .def_readwrite("attr", &Pet::attr);
+
+    py::enum_<Pet::Kind>(pet, "Kind")
+        .value("Dog", Pet::Kind::Dog)
+        .value("Cat", Pet::Kind::Cat)
+        .export_values();
+
+    py::class_<Pet::Attributes>(pet, "Attributes")
+        .def(py::init<>())
+        .def_readwrite("age", &Pet::Attributes::age);
+
+
+To ensure that the nested types ``Kind`` and ``Attributes`` are created within the scope of ``Pet``, the
+``pet`` ``py::class_`` instance must be supplied to the :class:`enum_` and ``py::class_``
+constructor. The :func:`enum_::export_values` function exports the enum entries
+into the parent scope, which should be skipped for newer C++11-style strongly
+typed enums.
+
+.. code-block:: pycon
+
+    >>> p = Pet("Lucy", Pet.Cat)
+    >>> p.type
+    Kind.Cat
+    >>> int(p.type)
+    1L
+
+The entries defined by the enumeration type are exposed in the ``__members__`` property:
+
+.. code-block:: pycon
+
+    >>> Pet.Kind.__members__
+    {'Dog': Kind.Dog, 'Cat': Kind.Cat}
+
+The ``name`` property returns the name of the enum value as a unicode string.
+
+.. note::
+
+    It is also possible to use ``str(enum)``, however these accomplish different
+    goals. The following shows how these two approaches differ.
+
+    .. code-block:: pycon
+
+        >>> p = Pet("Lucy", Pet.Cat)
+        >>> pet_type = p.type
+        >>> pet_type
+        Pet.Cat
+        >>> str(pet_type)
+        'Pet.Cat'
+        >>> pet_type.name
+        'Cat'
+
+.. note::
+
+    When the special tag ``py::arithmetic()`` is specified to the ``enum_``
+    constructor, pybind11 creates an enumeration that also supports rudimentary
+    arithmetic and bit-level operations like comparisons, and, or, xor, negation,
+    etc.
+
+    .. code-block:: cpp
+
+        py::enum_<Pet::Kind>(pet, "Kind", py::arithmetic())
+           ...
+
+    By default, these are omitted to conserve space.
+
+.. warning::
+
+    Contrary to Python customs, enum values from the wrappers should not be compared using ``is``, but with ``==`` (see `#1177 <https://github.com/pybind/pybind11/issues/1177>`_ for background).
diff --git a/docs/classes.rst b/docs/classes.rst
index 5406668f0b..e68b802ee9 100644
--- a/docs/classes.rst
+++ b/docs/classes.rst
@@ -459,6 +459,8 @@ you can use ``py::detail::overload_cast_impl`` with an additional set of parenth
     other using the ``.def(py::init<...>())`` syntax. The existing machinery
     for specifying keyword and default arguments also works.
 
+.. _native_enum:
+
 Enumerations and internal types
 ===============================
 
@@ -487,6 +489,8 @@ The binding code for this example looks as follows:
 
 .. code-block:: cpp
 
+    #include <pybind11/native_enum.h> // Not already included with pybind11.h
+
     py::class_<Pet> pet(m, "Pet");
 
     pet.def(py::init<const std::string &, Pet::Kind>())
@@ -494,69 +498,93 @@ The binding code for this example looks as follows:
         .def_readwrite("type", &Pet::type)
         .def_readwrite("attr", &Pet::attr);
 
-    py::enum_<Pet::Kind>(pet, "Kind")
+    py::native_enum<Pet::Kind>(pet, "Kind")
         .value("Dog", Pet::Kind::Dog)
         .value("Cat", Pet::Kind::Cat)
-        .export_values();
+        .export_values()
+        .finalize();
 
     py::class_<Pet::Attributes>(pet, "Attributes")
         .def(py::init<>())
         .def_readwrite("age", &Pet::Attributes::age);
 
 
-To ensure that the nested types ``Kind`` and ``Attributes`` are created within the scope of ``Pet``, the
-``pet`` ``py::class_`` instance must be supplied to the :class:`enum_` and ``py::class_``
-constructor. The :func:`enum_::export_values` function exports the enum entries
-into the parent scope, which should be skipped for newer C++11-style strongly
-typed enums.
+To ensure that the nested types ``Kind`` and ``Attributes`` are created
+within the scope of ``Pet``, the ``pet`` ``py::class_`` instance must be
+supplied to the ``py::native_enum`` and ``py::class_`` constructors. The
+``.export_values()`` function is available for exporting the enum entries
+into the parent scope, if desired.
 
-.. code-block:: pycon
+``py::native_enum`` was introduced with pybind11v3. It binds C++ enum types
+to native Python enum types, typically types in Python's
+`stdlib enum <https://docs.python.org/3/library/enum.html>`_ module,
+which are `PEP 435 compatible <https://peps.python.org/pep-0435/>`_.
+This is the recommended way to bind C++ enums.
+The older ``py::enum_`` is not PEP 435 compatible
+(see `issue #2332 <https://github.com/pybind/pybind11/issues/2332>`_)
+but remains supported indefinitely for backward compatibility.
+New bindings should prefer ``py::native_enum``.
 
-    >>> p = Pet("Lucy", Pet.Cat)
-    >>> p.type
-    Kind.Cat
-    >>> int(p.type)
-    1L
+.. note::
 
-The entries defined by the enumeration type are exposed in the ``__members__`` property:
+    The deprecated ``py::enum_`` is :ref:`documented here <deprecated_enum>`.
 
-.. code-block:: pycon
+The ``.finalize()`` call above is needed because Python's native enums
+cannot be built incrementally — all name/value pairs need to be passed at
+once. To achieve this, ``py::native_enum`` acts as a buffer to collect the
+name/value pairs. The ``.finalize()`` call uses the accumulated name/value
+pairs to build the arguments for constructing a native Python enum type.
 
-    >>> Pet.Kind.__members__
-    {'Dog': Kind.Dog, 'Cat': Kind.Cat}
+The ``py::native_enum`` constructor supports a third optional
+``native_type_name`` string argument, with default value ``"enum.Enum"``.
+Other types can be specified like this:
 
-The ``name`` property returns the name of the enum value as a unicode string.
+.. code-block:: cpp
 
-.. note::
+    py::native_enum<Pet::Kind>(pet, "Kind", "enum.IntEnum")
 
-    It is also possible to use ``str(enum)``, however these accomplish different
-    goals. The following shows how these two approaches differ.
+Any fully-qualified Python name can be specified. The only requirement is
+that the named type is similar to
+`enum.Enum <https://docs.python.org/3/library/enum.html#enum.Enum>`_
+in these ways:
 
-    .. code-block:: pycon
+* Has a `constructor similar to that of enum.Enum
+  <https://docs.python.org/3/howto/enum.html#functional-api>`_::
 
-        >>> p = Pet("Lucy", Pet.Cat)
-        >>> pet_type = p.type
-        >>> pet_type
-        Pet.Cat
-        >>> str(pet_type)
-        'Pet.Cat'
-        >>> pet_type.name
-        'Cat'
+    Colors = enum.Enum("Colors", (("Red", 0), ("Green", 1)))
+
+* A `C++ underlying <https://en.cppreference.com/w/cpp/types/underlying_type>`_
+  enum value can be passed to the constructor for the Python enum value::
+
+    red = Colors(0)
+
+* The enum values have a ``.value`` property yielding a value that
+  can be cast to the C++ underlying type::
+
+    underlying = red.value
+
+As of Python 3.13, the compatible `types in the stdlib enum module
+<https://docs.python.org/3/library/enum.html#module-contents>`_ are:
+``Enum``, ``IntEnum``, ``Flag``, ``IntFlag``.
 
 .. note::
 
-    When the special tag ``py::arithmetic()`` is specified to the ``enum_``
-    constructor, pybind11 creates an enumeration that also supports rudimentary
-    arithmetic and bit-level operations like comparisons, and, or, xor, negation,
-    etc.
+    In rare cases, a C++ enum may be bound to Python via a
+    :ref:`custom type caster <custom_type_caster>`. In such cases, a
+    template specialization like this may be required:
 
     .. code-block:: cpp
 
-        py::enum_<Pet::Kind>(pet, "Kind", py::arithmetic())
-           ...
-
-    By default, these are omitted to conserve space.
+        #if defined(PYBIND11_HAS_NATIVE_ENUM)
+        namespace pybind11::detail {
+        template <typename FancyEnum>
+        struct type_caster_enum_type_enabled<
+            FancyEnum,
+            std::enable_if_t<is_fancy_enum<FancyEnum>::value>> : std::false_type {};
+        }
+        #endif
 
-.. warning::
+    This specialization is needed only if the custom type caster is templated.
 
-    Contrary to Python customs, enum values from the wrappers should not be compared using ``is``, but with ``==`` (see `#1177 <https://github.com/pybind/pybind11/issues/1177>`_ for background).
+    The ``PYBIND11_HAS_NATIVE_ENUM`` guard is needed only if backward
+    compatibility with pybind11v2 is required.
diff --git a/docs/index.rst b/docs/index.rst
index 4e2e8ca3a0..77b097c574 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -36,6 +36,7 @@
    advanced/pycpp/index
    advanced/embedding
    advanced/misc
+   advanced/deprecated
 
 .. toctree::
    :caption: Extra Information
diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h
index 89d3d43a7a..4102152447 100644
--- a/include/pybind11/cast.h
+++ b/include/pybind11/cast.h
@@ -12,6 +12,7 @@
 
 #include "detail/common.h"
 #include "detail/descr.h"
+#include "detail/native_enum_data.h"
 #include "detail/type_caster_base.h"
 #include "detail/typeid.h"
 #include "pytypes.h"
@@ -53,6 +54,104 @@ cast_op(make_caster<T> &&caster) {
     return std::move(caster).operator result_t();
 }
 
+template <typename EnumType>
+class type_caster_enum_type {
+private:
+    using Underlying = typename std::underlying_type<EnumType>::type;
+
+public:
+    static constexpr auto name = const_name<EnumType>();
+
+    template <typename SrcType>
+    static handle cast(SrcType &&src, return_value_policy, handle parent) {
+        handle native_enum
+            = global_internals_native_enum_type_map_get_item(std::type_index(typeid(EnumType)));
+        if (native_enum) {
+            return native_enum(static_cast<Underlying>(src)).release();
+        }
+        return type_caster_base<EnumType>::cast(
+            std::forward<SrcType>(src),
+            // Fixes https://github.com/pybind/pybind11/pull/3643#issuecomment-1022987818:
+            return_value_policy::copy,
+            parent);
+    }
+
+    bool load(handle src, bool convert) {
+        handle native_enum
+            = global_internals_native_enum_type_map_get_item(std::type_index(typeid(EnumType)));
+        if (native_enum) {
+            if (!isinstance(src, native_enum)) {
+                return false;
+            }
+            type_caster<Underlying> underlying_caster;
+            if (!underlying_caster.load(src.attr("value"), convert)) {
+                pybind11_fail("native_enum internal consistency failure.");
+            }
+            value = static_cast<EnumType>(static_cast<Underlying>(underlying_caster));
+            return true;
+        }
+        if (!pybind11_enum_) {
+            pybind11_enum_.reset(new type_caster_base<EnumType>());
+        }
+        return pybind11_enum_->load(src, convert);
+    }
+
+    template <typename T>
+    using cast_op_type = detail::cast_op_type<T>;
+
+    // NOLINTNEXTLINE(google-explicit-constructor)
+    operator EnumType *() {
+        if (!pybind11_enum_) {
+            return &value;
+        }
+        return pybind11_enum_->operator EnumType *();
+    }
+
+    // NOLINTNEXTLINE(google-explicit-constructor)
+    operator EnumType &() {
+        if (!pybind11_enum_) {
+            return value;
+        }
+        return pybind11_enum_->operator EnumType &();
+    }
+
+private:
+    std::unique_ptr<type_caster_base<EnumType>> pybind11_enum_;
+    EnumType value;
+};
+
+template <typename EnumType, typename SFINAE = void>
+struct type_caster_enum_type_enabled : std::true_type {};
+
+template <typename T>
+struct type_uses_type_caster_enum_type {
+    static constexpr bool value
+        = std::is_enum<T>::value && type_caster_enum_type_enabled<T>::value;
+};
+
+template <typename EnumType>
+class type_caster<EnumType, detail::enable_if_t<type_uses_type_caster_enum_type<EnumType>::value>>
+    : public type_caster_enum_type<EnumType> {};
+
+template <typename T, detail::enable_if_t<std::is_enum<T>::value, int> = 0>
+bool isinstance_native_enum_impl(handle obj, const std::type_info &tp) {
+    handle native_enum = global_internals_native_enum_type_map_get_item(tp);
+    if (!native_enum) {
+        return false;
+    }
+    return isinstance(obj, native_enum);
+}
+
+template <typename T, detail::enable_if_t<!std::is_enum<T>::value, int> = 0>
+bool isinstance_native_enum_impl(handle, const std::type_info &) {
+    return false;
+}
+
+template <typename T>
+bool isinstance_native_enum(handle obj, const std::type_info &tp) {
+    return isinstance_native_enum_impl<intrinsic_t<T>>(obj, tp);
+}
+
 template <typename type>
 class type_caster<std::reference_wrapper<type>> {
 private:
@@ -1470,8 +1569,17 @@ template <typename T,
           = 0>
 T cast(const handle &handle) {
     using namespace detail;
-    static_assert(!cast_is_temporary_value_reference<T>::value,
+    constexpr bool is_enum_cast = type_uses_type_caster_enum_type<intrinsic_t<T>>::value;
+    static_assert(!cast_is_temporary_value_reference<T>::value || is_enum_cast,
                   "Unable to cast type to reference: value is local to type caster");
+#ifndef NDEBUG
+    if (is_enum_cast && cast_is_temporary_value_reference<T>::value) {
+        if (detail::global_internals_native_enum_type_map_contains(
+                std::type_index(typeid(intrinsic_t<T>)))) {
+            pybind11_fail("Unable to cast native enum type to reference");
+        }
+    }
+#endif
     return cast_op<T>(load_type<T>(handle));
 }
 
diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h
index 08e23afb59..98ff61f5d7 100644
--- a/include/pybind11/detail/class.h
+++ b/include/pybind11/detail/class.h
@@ -716,15 +716,7 @@ inline PyObject *make_new_python_type(const type_record &rec) {
             PyUnicode_FromFormat("%U.%U", rec.scope.attr("__qualname__").ptr(), name.ptr()));
     }
 
-    object module_;
-    if (rec.scope) {
-        if (hasattr(rec.scope, "__module__")) {
-            module_ = rec.scope.attr("__module__");
-        } else if (hasattr(rec.scope, "__name__")) {
-            module_ = rec.scope.attr("__name__");
-        }
-    }
-
+    object module_ = get_module_name_if_available(rec.scope);
     const auto *full_name = c_str(
 #if !defined(PYPY_VERSION)
         module_ ? str(module_).cast<std::string>() + "." + rec.name :
diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h
index 841c8fe155..2fd1f6719c 100644
--- a/include/pybind11/detail/internals.h
+++ b/include/pybind11/detail/internals.h
@@ -37,11 +37,11 @@
 /// further ABI-incompatible changes may be made before the ABI is officially
 /// changed to the new version.
 #ifndef PYBIND11_INTERNALS_VERSION
-#    define PYBIND11_INTERNALS_VERSION 7
+#    define PYBIND11_INTERNALS_VERSION 8
 #endif
 
-#if PYBIND11_INTERNALS_VERSION < 7
-#    error "PYBIND11_INTERNALS_VERSION 7 is the minimum for all platforms for pybind11v3."
+#if PYBIND11_INTERNALS_VERSION < 8
+#    error "PYBIND11_INTERNALS_VERSION 8 is the minimum for all platforms for pybind11v3."
 #endif
 
 PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
@@ -194,6 +194,8 @@ struct internals {
     // We want unique addresses since we use pointer equality to compare function records
     std::string function_record_capsule_name = internals_function_record_capsule_name;
 
+    type_map<PyObject *> native_enum_type_map;
+
     internals() = default;
     internals(const internals &other) = delete;
     internals &operator=(const internals &other) = delete;
diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h
new file mode 100644
index 0000000000..5192ed51a6
--- /dev/null
+++ b/include/pybind11/detail/native_enum_data.h
@@ -0,0 +1,201 @@
+// Copyright (c) 2022-2025 The pybind Community.
+// All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+#pragma once
+
+#define PYBIND11_HAS_NATIVE_ENUM
+
+#include "../pytypes.h"
+#include "common.h"
+#include "internals.h"
+
+#include <cassert>
+#include <sstream>
+#include <string>
+#include <typeindex>
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+PYBIND11_NAMESPACE_BEGIN(detail)
+
+// This is a separate function only to enable easy unit testing.
+inline std::string
+native_enum_missing_finalize_error_message(const std::string &enum_name_encoded) {
+    return "pybind11::native_enum<...>(\"" + enum_name_encoded + "\", ...): MISSING .finalize()";
+}
+
+class native_enum_data {
+public:
+    native_enum_data(const object &parent_scope,
+                     const char *enum_name,
+                     const char *native_type_name,
+                     const std::type_index &enum_type_index)
+        : enum_name_encoded{enum_name}, native_type_name_encoded{native_type_name},
+          enum_type_index{enum_type_index}, parent_scope(parent_scope), enum_name{enum_name},
+          native_type_name{native_type_name}, export_values_flag{false}, finalize_needed{false} {}
+
+    void finalize();
+
+    native_enum_data(const native_enum_data &) = delete;
+    native_enum_data &operator=(const native_enum_data &) = delete;
+
+#if !defined(NDEBUG)
+    // This dtor cannot easily be unit tested because it terminates the process.
+    ~native_enum_data() {
+        if (finalize_needed) {
+            pybind11_fail(native_enum_missing_finalize_error_message(enum_name_encoded));
+        }
+    }
+#endif
+
+protected:
+    void disarm_finalize_check(const char *error_context) {
+        if (!finalize_needed) {
+            pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded
+                          + "\"): " + error_context);
+        }
+        finalize_needed = false;
+    }
+
+    void arm_finalize_check() {
+        assert(!finalize_needed); // Catch redundant calls.
+        finalize_needed = true;
+    }
+
+    std::string enum_name_encoded;
+    std::string native_type_name_encoded;
+    std::type_index enum_type_index;
+
+private:
+    object parent_scope;
+    str enum_name;
+    str native_type_name;
+
+protected:
+    list members;
+    list docs;
+    bool export_values_flag : 1; // Attention: It is best to keep the bools together.
+
+private:
+    bool finalize_needed : 1;
+};
+
+inline void global_internals_native_enum_type_map_set_item(const std::type_index &enum_type_index,
+                                                           PyObject *py_enum) {
+    with_internals(
+        [&](internals &internals) { internals.native_enum_type_map[enum_type_index] = py_enum; });
+}
+
+inline handle
+global_internals_native_enum_type_map_get_item(const std::type_index &enum_type_index) {
+    return with_internals([&](internals &internals) {
+        auto found = internals.native_enum_type_map.find(enum_type_index);
+        if (found != internals.native_enum_type_map.end()) {
+            return handle(found->second);
+        }
+        return handle();
+    });
+}
+
+inline bool
+global_internals_native_enum_type_map_contains(const std::type_index &enum_type_index) {
+    return with_internals([&](internals &internals) {
+        return internals.native_enum_type_map.count(enum_type_index) != 0;
+    });
+}
+
+inline object import_or_getattr(const std::string &fully_qualified_name,
+                                const std::string &append_to_exception_message) {
+    std::istringstream stream(fully_qualified_name);
+    std::string part;
+
+    if (!std::getline(stream, part, '.') || part.empty()) {
+        std::string msg = "Invalid fully-qualified name `";
+        msg += fully_qualified_name;
+        msg += "`";
+        msg += append_to_exception_message;
+        throw value_error(msg);
+    }
+
+    auto curr_scope = reinterpret_steal<object>(PyImport_ImportModule(part.c_str()));
+    if (!curr_scope) {
+        std::string msg = "Failed to import top-level module `";
+        msg += part;
+        msg += "`";
+        msg += append_to_exception_message;
+        raise_from(PyExc_ImportError, msg.c_str());
+        throw error_already_set();
+    }
+
+    // Now recursively getattr or import remaining parts
+    std::string curr_path = part;
+    while (std::getline(stream, part, '.')) {
+        if (part.empty()) {
+            std::string msg = "Invalid fully-qualified name `";
+            msg += fully_qualified_name;
+            msg += "`";
+            msg += append_to_exception_message;
+            throw value_error(msg);
+        }
+        std::string next_path = curr_path;
+        next_path += ".";
+        next_path += part;
+        auto next_scope
+            = reinterpret_steal<object>(PyObject_GetAttrString(curr_scope.ptr(), part.c_str()));
+        if (!next_scope) {
+            error_fetch_and_normalize stored_getattr_error("getattr");
+            // Try importing the next level
+            next_scope = reinterpret_steal<object>(PyImport_ImportModule(next_path.c_str()));
+            if (!next_scope) {
+                error_fetch_and_normalize stored_import_error("import");
+                std::string msg = "Failed to import or getattr `";
+                msg += part;
+                msg += "` from `";
+                msg += curr_path;
+                msg += "`";
+                msg += append_to_exception_message;
+                msg += "\n-------- getattr exception --------\n";
+                msg += stored_getattr_error.error_string();
+                msg += "\n-------- import exception --------\n";
+                msg += stored_import_error.error_string();
+                throw import_error(msg.c_str());
+            }
+        }
+        curr_scope = next_scope;
+        curr_path = next_path;
+    }
+    return curr_scope;
+}
+
+inline void native_enum_data::finalize() {
+    disarm_finalize_check("DOUBLE finalize");
+    if (hasattr(parent_scope, enum_name)) {
+        pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded
+                      + "\"): an object with that name is already defined");
+    }
+    auto py_enum_type = import_or_getattr(native_type_name, " (native_type_name)");
+    auto py_enum = py_enum_type(enum_name, members);
+    object module_name = get_module_name_if_available(parent_scope);
+    if (module_name) {
+        py_enum.attr("__module__") = module_name;
+    }
+    parent_scope.attr(enum_name) = py_enum;
+    if (export_values_flag) {
+        for (auto member : members) {
+            auto member_name = member[int_(0)];
+            if (hasattr(parent_scope, member_name)) {
+                pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded + "\").value(\""
+                              + member_name.cast<std::string>()
+                              + "\"): an object with that name is already defined");
+            }
+            parent_scope.attr(member_name) = py_enum[member_name];
+        }
+    }
+    for (auto doc : docs) {
+        py_enum[doc[int_(0)]].attr("__doc__") = doc[int_(1)];
+    }
+    global_internals_native_enum_type_map_set_item(enum_type_index, py_enum.release().ptr());
+}
+
+PYBIND11_NAMESPACE_END(detail)
+PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h
new file mode 100644
index 0000000000..759acd48db
--- /dev/null
+++ b/include/pybind11/native_enum.h
@@ -0,0 +1,66 @@
+// Copyright (c) 2022-2025 The pybind Community.
+// All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+#pragma once
+
+#include "detail/common.h"
+#include "detail/native_enum_data.h"
+#include "detail/type_caster_base.h"
+#include "cast.h"
+
+#include <cassert>
+#include <limits>
+#include <type_traits>
+#include <typeindex>
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+
+/// Conversions between Python's native (stdlib) enum types and C++ enums.
+template <typename EnumType>
+class native_enum : public detail::native_enum_data {
+public:
+    using Underlying = typename std::underlying_type<EnumType>::type;
+
+    native_enum(const object &parent_scope,
+                const char *name,
+                const char *native_type_name = "enum.Enum")
+        : detail::native_enum_data(
+              parent_scope, name, native_type_name, std::type_index(typeid(EnumType))) {
+        if (detail::get_local_type_info(typeid(EnumType)) != nullptr
+            || detail::get_global_type_info(typeid(EnumType)) != nullptr) {
+            pybind11_fail(
+                "pybind11::native_enum<...>(\"" + enum_name_encoded
+                + "\") is already registered as a `pybind11::enum_` or `pybind11::class_`!");
+        }
+        if (detail::global_internals_native_enum_type_map_contains(enum_type_index)) {
+            pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded
+                          + "\") is already registered!");
+        }
+        arm_finalize_check();
+    }
+
+    /// Export enumeration entries into the parent scope
+    native_enum &export_values() {
+        assert(!export_values_flag); // Catch redundant calls.
+        export_values_flag = true;
+        return *this;
+    }
+
+    /// Add an enumeration entry
+    native_enum &value(char const *name, EnumType value, const char *doc = nullptr) {
+        // Disarm for the case that the native_enum_data dtor runs during exception unwinding.
+        disarm_finalize_check("value after finalize");
+        members.append(make_tuple(name, static_cast<Underlying>(value)));
+        if (doc) {
+            docs.append(make_tuple(name, doc));
+        }
+        arm_finalize_check(); // There was no exception.
+        return *this;
+    }
+
+    native_enum(const native_enum &) = delete;
+    native_enum &operator=(const native_enum &) = delete;
+};
+
+PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h
index cf11e373dc..caca57b3e4 100644
--- a/include/pybind11/pybind11.h
+++ b/include/pybind11/pybind11.h
@@ -13,6 +13,7 @@
 #include "detail/dynamic_raw_ptr_cast_if_possible.h"
 #include "detail/exception_translation.h"
 #include "detail/init.h"
+#include "detail/native_enum_data.h"
 #include "detail/using_smart_holder.h"
 #include "attr.h"
 #include "gil.h"
@@ -2768,6 +2769,14 @@ class enum_ : public class_<Type> {
     template <typename... Extra>
     enum_(const handle &scope, const char *name, const Extra &...extra)
         : class_<Type>(scope, name, extra...), m_base(*this, scope) {
+        {
+            if (detail::global_internals_native_enum_type_map_contains(
+                    std::type_index(typeid(Type)))) {
+                pybind11_fail("pybind11::enum_ \"" + std::string(name)
+                              + "\" is already registered as a pybind11::native_enum!");
+            }
+        }
+
         constexpr bool is_arithmetic = detail::any_of<std::is_same<arithmetic, Extra>...>::value;
         constexpr bool is_convertible = std::is_convertible<Type, Underlying>::value;
         m_base.init(is_arithmetic, is_convertible);
diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h
index 1147cab8ec..57f1279657 100644
--- a/include/pybind11/pytypes.h
+++ b/include/pybind11/pytypes.h
@@ -48,6 +48,9 @@ PYBIND11_NAMESPACE_BEGIN(detail)
 class args_proxy;
 bool isinstance_generic(handle obj, const std::type_info &tp);
 
+template <typename T>
+bool isinstance_native_enum(handle obj, const std::type_info &tp);
+
 // Accessor forward declarations
 template <typename Policy>
 class accessor;
@@ -852,7 +855,8 @@ bool isinstance(handle obj) {
 
 template <typename T, detail::enable_if_t<!std::is_base_of<object, T>::value, int> = 0>
 bool isinstance(handle obj) {
-    return detail::isinstance_generic(obj, typeid(T));
+    return detail::isinstance_native_enum<T>(obj, typeid(T))
+           || detail::isinstance_generic(obj, typeid(T));
 }
 
 template <>
@@ -2644,5 +2648,18 @@ PYBIND11_MATH_OPERATOR_BINARY_INPLACE(operator>>=, PyNumber_InPlaceRshift)
 #undef PYBIND11_MATH_OPERATOR_BINARY
 #undef PYBIND11_MATH_OPERATOR_BINARY_INPLACE
 
+// Meant to return a Python str, but this is not checked.
+inline object get_module_name_if_available(handle scope) {
+    if (scope) {
+        if (hasattr(scope, "__module__")) {
+            return scope.attr("__module__");
+        }
+        if (hasattr(scope, "__name__")) {
+            return scope.attr("__name__");
+        }
+    }
+    return object();
+}
+
 PYBIND11_NAMESPACE_END(detail)
 PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e2fab9c13d..1b65729dfd 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -154,6 +154,7 @@ set(PYBIND11_TEST_FILES
     test_methods_and_attributes
     test_modules
     test_multiple_inheritance
+    test_native_enum
     test_numpy_array
     test_numpy_dtypes
     test_numpy_vectorize
diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py
index 5d7299c2ac..2fa47c614a 100644
--- a/tests/extra_python_package/test_files.py
+++ b/tests/extra_python_package/test_files.py
@@ -38,6 +38,7 @@
     "include/pybind11/gil.h",
     "include/pybind11/gil_safe_call_once.h",
     "include/pybind11/iostream.h",
+    "include/pybind11/native_enum.h",
     "include/pybind11/numpy.h",
     "include/pybind11/operators.h",
     "include/pybind11/options.h",
@@ -66,6 +67,7 @@
     "include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h",
     "include/pybind11/detail/init.h",
     "include/pybind11/detail/internals.h",
+    "include/pybind11/detail/native_enum_data.h",
     "include/pybind11/detail/struct_smart_holder.h",
     "include/pybind11/detail/type_caster_base.h",
     "include/pybind11/detail/typeid.h",
diff --git a/tests/test_enum.cpp b/tests/test_enum.cpp
index 2597b275ef..4ec0af7245 100644
--- a/tests/test_enum.cpp
+++ b/tests/test_enum.cpp
@@ -130,4 +130,20 @@ TEST_SUBMODULE(enums, m) {
     py::enum_<ScopedBoolEnum>(m, "ScopedBoolEnum")
         .value("FALSE", ScopedBoolEnum::FALSE)
         .value("TRUE", ScopedBoolEnum::TRUE);
+
+#if defined(__MINGW32__)
+    m.attr("obj_cast_UnscopedEnum_ptr") = "MinGW: dangling pointer to an unnamed temporary may be "
+                                          "used [-Werror=dangling-pointer=]";
+#else
+    m.def("obj_cast_UnscopedEnum_ptr", [](const py::object &obj) {
+        // https://github.com/OpenImageIO/oiio/blob/30ea4ebdfab11aec291befbaff446f2a7d24835b/src/python/py_oiio.h#L300
+        if (py::isinstance<UnscopedEnum>(obj)) {
+            if (*obj.cast<UnscopedEnum *>() == UnscopedEnum::ETwo) {
+                return 2;
+            }
+            return 1;
+        }
+        return 0;
+    });
+#endif
 }
diff --git a/tests/test_enum.py b/tests/test_enum.py
index 044ef1803e..697e28843f 100644
--- a/tests/test_enum.py
+++ b/tests/test_enum.py
@@ -331,3 +331,12 @@ def test_generated_dunder_methods_pos_only():
             )
             is not None
         )
+
+
+@pytest.mark.skipif(
+    isinstance(m.obj_cast_UnscopedEnum_ptr, str), reason=m.obj_cast_UnscopedEnum_ptr
+)
+def test_obj_cast_unscoped_enum_ptr():
+    assert m.obj_cast_UnscopedEnum_ptr(m.UnscopedEnum.ETwo) == 2
+    assert m.obj_cast_UnscopedEnum_ptr(m.UnscopedEnum.EOne) == 1
+    assert m.obj_cast_UnscopedEnum_ptr(None) == 0
diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp
new file mode 100644
index 0000000000..1a4c3180c7
--- /dev/null
+++ b/tests/test_native_enum.cpp
@@ -0,0 +1,234 @@
+#include <pybind11/native_enum.h>
+
+#include "pybind11_tests.h"
+
+#include <typeindex>
+
+namespace test_native_enum {
+
+// https://en.cppreference.com/w/cpp/language/enum
+
+// enum that takes 16 bits
+enum smallenum : std::int16_t { a, b, c };
+
+// color may be red (value 0), yellow (value 1), green (value 20), or blue (value 21)
+enum color { red, yellow, green = 20, blue };
+
+// altitude may be altitude::high or altitude::low
+enum class altitude : char {
+    high = 'h',
+    low = 'l', // trailing comma only allowed after CWG518
+};
+
+enum class flags_uchar : unsigned char { bit0 = 0x1u, bit1 = 0x2u, bit2 = 0x4u };
+enum class flags_uint : unsigned int { bit0 = 0x1u, bit1 = 0x2u, bit2 = 0x4u };
+
+enum class export_values { exv0, exv1 };
+
+enum class member_doc { mem0, mem1, mem2 };
+
+struct class_with_enum {
+    enum class in_class { one, two };
+};
+
+// https://github.com/protocolbuffers/protobuf/blob/d70b5c5156858132decfdbae0a1103e6a5cb1345/src/google/protobuf/generated_enum_util.h#L52-L53
+template <typename T>
+struct is_proto_enum : std::false_type {};
+
+enum some_proto_enum : int { Zero, One };
+
+template <>
+struct is_proto_enum<some_proto_enum> : std::true_type {};
+
+} // namespace test_native_enum
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+PYBIND11_NAMESPACE_BEGIN(detail)
+
+template <typename ProtoEnumType>
+struct type_caster_enum_type_enabled<
+    ProtoEnumType,
+    detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> : std::false_type {
+};
+
+// https://github.com/pybind/pybind11_protobuf/blob/a50899c2eb604fc5f25deeb8901eff6231b8b3c0/pybind11_protobuf/enum_type_caster.h#L101-L105
+template <typename ProtoEnumType>
+struct type_caster<ProtoEnumType,
+                   detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> {
+    static handle
+    cast(const ProtoEnumType & /*src*/, return_value_policy /*policy*/, handle /*parent*/) {
+        return py::none();
+    }
+
+    bool load(handle /*src*/, bool /*convert*/) {
+        value = static_cast<ProtoEnumType>(0);
+        return true;
+    }
+
+    PYBIND11_TYPE_CASTER(ProtoEnumType, const_name<ProtoEnumType>());
+};
+
+PYBIND11_NAMESPACE_END(detail)
+PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
+
+TEST_SUBMODULE(native_enum, m) {
+    using namespace test_native_enum;
+
+    py::native_enum<smallenum>(m, "smallenum", "enum.IntEnum")
+        .value("a", smallenum::a)
+        .value("b", smallenum::b)
+        .value("c", smallenum::c)
+        .finalize();
+
+    py::native_enum<color>(m, "color", "enum.IntEnum")
+        .value("red", color::red)
+        .value("yellow", color::yellow)
+        .value("green", color::green)
+        .value("blue", color::blue)
+        .finalize();
+
+    py::native_enum<altitude>(m, "altitude")
+        .value("high", altitude::high)
+        .value("low", altitude::low)
+        .finalize();
+
+    py::native_enum<flags_uchar>(m, "flags_uchar", "enum.Flag")
+        .value("bit0", flags_uchar::bit0)
+        .value("bit1", flags_uchar::bit1)
+        .value("bit2", flags_uchar::bit2)
+        .finalize();
+
+    py::native_enum<flags_uint>(m, "flags_uint", "enum.IntFlag")
+        .value("bit0", flags_uint::bit0)
+        .value("bit1", flags_uint::bit1)
+        .value("bit2", flags_uint::bit2)
+        .finalize();
+
+    py::native_enum<export_values>(m, "export_values", "enum.IntEnum")
+        .value("exv0", export_values::exv0)
+        .value("exv1", export_values::exv1)
+        .export_values()
+        .finalize();
+
+    py::native_enum<member_doc>(m, "member_doc", "enum.IntEnum")
+        .value("mem0", member_doc::mem0, "docA")
+        .value("mem1", member_doc::mem1)
+        .value("mem2", member_doc::mem2, "docC")
+        .finalize();
+
+    py::class_<class_with_enum> py_class_with_enum(m, "class_with_enum");
+    py::native_enum<class_with_enum::in_class>(py_class_with_enum, "in_class", "enum.IntEnum")
+        .value("one", class_with_enum::in_class::one)
+        .value("two", class_with_enum::in_class::two)
+        .finalize();
+
+    m.def("isinstance_color", [](const py::object &obj) { return py::isinstance<color>(obj); });
+
+    m.def("pass_color", [](color e) { return static_cast<int>(e); });
+    m.def("return_color", [](int i) { return static_cast<color>(i); });
+
+    m.def("pass_some_proto_enum", [](some_proto_enum) { return py::none(); });
+    m.def("return_some_proto_enum", []() { return some_proto_enum::Zero; });
+
+#if defined(__MINGW32__)
+    m.attr("obj_cast_color_ptr") = "MinGW: dangling pointer to an unnamed temporary may be used "
+                                   "[-Werror=dangling-pointer=]";
+#elif defined(NDEBUG)
+    m.attr("obj_cast_color_ptr") = "NDEBUG disables cast safety check";
+#else
+    m.def("obj_cast_color_ptr", [](const py::object &obj) { obj.cast<color *>(); });
+#endif
+
+    m.def("py_cast_color_handle", [](py::handle obj) {
+        // Exercises `if (is_enum_cast && cast_is_temporary_value_reference<T>::value)`
+        // in `T cast(const handle &handle)`
+        auto e = py::cast<color>(obj);
+        return static_cast<int>(e);
+    });
+
+    m.def("exercise_import_or_getattr", [](py::module_ &m, const char *native_type_name) {
+        enum fake { x };
+        py::native_enum<fake>(m, "fake_import_or_getattr", native_type_name)
+            .value("x", fake::x)
+            .finalize();
+    });
+
+    m.def("native_enum_data_missing_finalize_error_message",
+          [](const std::string &enum_name_encoded) {
+              return py::detail::native_enum_missing_finalize_error_message(enum_name_encoded);
+          });
+
+    m.def("native_enum_ctor_malformed_utf8", [](const char *malformed_utf8) {
+        enum fake { x };
+        py::native_enum<fake>{py::none(), malformed_utf8, "enum.IntEnum"};
+    });
+
+    m.def("native_enum_double_finalize", [](py::module_ &m) {
+        enum fake { x };
+        py::native_enum<fake> ne(m, "fake_native_enum_double_finalize", "enum.IntEnum");
+        ne.finalize();
+        ne.finalize();
+    });
+
+    m.def("native_enum_value_after_finalize", [](py::module_ &m) {
+        enum fake { x };
+        py::native_enum<fake> ne(m, "fake_native_enum_value_after_finalize", "enum.IntEnum");
+        ne.finalize();
+        ne.value("x", fake::x);
+    });
+
+    m.def("native_enum_value_malformed_utf8", [](const char *malformed_utf8) {
+        enum fake { x };
+        py::native_enum<fake>(py::none(), "fake", "enum.IntEnum").value(malformed_utf8, fake::x);
+    });
+
+    m.def("double_registration_native_enum", [](py::module_ &m) {
+        enum fake { x };
+        py::native_enum<fake>(m, "fake_double_registration_native_enum", "enum.IntEnum")
+            .value("x", fake::x)
+            .finalize();
+        py::native_enum<fake>(m, "fake_double_registration_native_enum");
+    });
+
+    m.def("native_enum_name_clash", [](py::module_ &m) {
+        enum fake { x };
+        py::native_enum<fake>(m, "fake_native_enum_name_clash", "enum.IntEnum")
+            .value("x", fake::x)
+            .finalize();
+    });
+
+    m.def("native_enum_value_name_clash", [](py::module_ &m) {
+        enum fake { x };
+        py::native_enum<fake>(m, "fake_native_enum_value_name_clash", "enum.IntEnum")
+            .value("fake_native_enum_value_name_clash_x", fake::x)
+            .export_values()
+            .finalize();
+    });
+
+    m.def("double_registration_enum_before_native_enum", [](py::module_ &m) {
+        enum fake { x };
+        py::enum_<fake>(m, "fake_enum_first").value("x", fake::x);
+        py::native_enum<fake>(m, "fake_enum_first", "enum.IntEnum").value("x", fake::x).finalize();
+    });
+
+    m.def("double_registration_native_enum_before_enum", [](py::module_ &m) {
+        enum fake { x };
+        py::native_enum<fake>(m, "fake_native_enum_first", "enum.IntEnum")
+            .value("x", fake::x)
+            .finalize();
+        py::enum_<fake>(m, "name_must_be_different_to_reach_desired_code_path");
+    });
+
+#if defined(PYBIND11_NEGATE_THIS_CONDITION_FOR_LOCAL_TESTING) && !defined(NDEBUG)
+    m.def("native_enum_missing_finalize_failure", []() {
+        enum fake { x };
+        py::native_enum<fake>(
+            py::none(), "fake_native_enum_missing_finalize_failure", "enum.IntEnum")
+            .value("x", fake::x)
+            // .finalize() missing
+            ;
+    });
+#else
+    m.attr("native_enum_missing_finalize_failure") = "For local testing only: terminates process";
+#endif
+}
diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py
new file mode 100644
index 0000000000..f98ebd0459
--- /dev/null
+++ b/tests/test_native_enum.py
@@ -0,0 +1,297 @@
+from __future__ import annotations
+
+import enum
+import pickle
+
+import pytest
+
+import env
+from pybind11_tests import native_enum as m
+
+SMALLENUM_MEMBERS = (
+    ("a", 0),
+    ("b", 1),
+    ("c", 2),
+)
+
+COLOR_MEMBERS = (
+    ("red", 0),
+    ("yellow", 1),
+    ("green", 20),
+    ("blue", 21),
+)
+
+ALTITUDE_MEMBERS = (
+    ("high", "h"),
+    ("low", "l"),
+)
+
+FLAGS_UCHAR_MEMBERS = (
+    ("bit0", 0x1),
+    ("bit1", 0x2),
+    ("bit2", 0x4),
+)
+
+FLAGS_UINT_MEMBERS = (
+    ("bit0", 0x1),
+    ("bit1", 0x2),
+    ("bit2", 0x4),
+)
+
+CLASS_WITH_ENUM_IN_CLASS_MEMBERS = (
+    ("one", 0),
+    ("two", 1),
+)
+
+EXPORT_VALUES_MEMBERS = (
+    ("exv0", 0),
+    ("exv1", 1),
+)
+
+MEMBER_DOC_MEMBERS = (
+    ("mem0", 0),
+    ("mem1", 1),
+    ("mem2", 2),
+)
+
+ENUM_TYPES_AND_MEMBERS = (
+    (m.smallenum, SMALLENUM_MEMBERS),
+    (m.color, COLOR_MEMBERS),
+    (m.altitude, ALTITUDE_MEMBERS),
+    (m.flags_uchar, FLAGS_UCHAR_MEMBERS),
+    (m.flags_uint, FLAGS_UINT_MEMBERS),
+    (m.export_values, EXPORT_VALUES_MEMBERS),
+    (m.member_doc, MEMBER_DOC_MEMBERS),
+    (m.class_with_enum.in_class, CLASS_WITH_ENUM_IN_CLASS_MEMBERS),
+)
+
+ENUM_TYPES = [_[0] for _ in ENUM_TYPES_AND_MEMBERS]
+
+
+@pytest.mark.parametrize("enum_type", ENUM_TYPES)
+def test_enum_type(enum_type):
+    assert isinstance(enum_type, enum.EnumMeta)
+    assert enum_type.__module__ == m.__name__
+
+
+@pytest.mark.parametrize(("enum_type", "members"), ENUM_TYPES_AND_MEMBERS)
+def test_enum_members(enum_type, members):
+    for name, value in members:
+        assert enum_type[name].value == value
+
+
+@pytest.mark.parametrize(("enum_type", "members"), ENUM_TYPES_AND_MEMBERS)
+def test_pickle_roundtrip(enum_type, members):
+    for name, _ in members:
+        orig = enum_type[name]
+        if enum_type is m.class_with_enum.in_class:
+            # This is a general pickle limitation.
+            with pytest.raises(pickle.PicklingError):
+                pickle.dumps(orig)
+        else:
+            # This only works if __module__ is correct.
+            serialized = pickle.dumps(orig)
+            restored = pickle.loads(serialized)
+            assert restored == orig
+
+
+@pytest.mark.parametrize("enum_type", [m.flags_uchar, m.flags_uint])
+def test_enum_flag(enum_type):
+    bits02 = enum_type.bit0 | enum_type.bit2
+    assert enum_type.bit0 in bits02
+    assert enum_type.bit1 not in bits02
+    assert enum_type.bit2 in bits02
+
+
+def test_export_values():
+    assert m.exv0 is m.export_values.exv0
+    assert m.exv1 is m.export_values.exv1
+
+
+def test_member_doc():
+    pure_native = enum.IntEnum("pure_native", (("mem", 0),))
+    assert m.member_doc.mem0.__doc__ == "docA"
+    assert m.member_doc.mem1.__doc__ == pure_native.mem.__doc__
+    assert m.member_doc.mem2.__doc__ == "docC"
+
+
+def test_pybind11_isinstance_color():
+    for name, _ in COLOR_MEMBERS:
+        assert m.isinstance_color(m.color[name])
+    assert not m.isinstance_color(m.color)
+    for name, _ in SMALLENUM_MEMBERS:
+        assert not m.isinstance_color(m.smallenum[name])
+    assert not m.isinstance_color(m.smallenum)
+    assert not m.isinstance_color(None)
+
+
+def test_pass_color_success():
+    for name, value in COLOR_MEMBERS:
+        assert m.pass_color(m.color[name]) == value
+
+
+def test_pass_color_fail():
+    with pytest.raises(TypeError) as excinfo:
+        m.pass_color(None)
+    assert "test_native_enum::color" in str(excinfo.value)
+
+
+def test_return_color_success():
+    for name, value in COLOR_MEMBERS:
+        assert m.return_color(value) == m.color[name]
+
+
+def test_return_color_fail():
+    with pytest.raises(ValueError) as excinfo_direct:
+        m.color(2)
+    with pytest.raises(ValueError) as excinfo_cast:
+        m.return_color(2)
+    assert str(excinfo_cast.value) == str(excinfo_direct.value)
+
+
+def test_type_caster_enum_type_enabled_false():
+    # This is really only a "does it compile" test.
+    assert m.pass_some_proto_enum(None) is None
+    assert m.return_some_proto_enum() is None
+
+
+@pytest.mark.skipif(isinstance(m.obj_cast_color_ptr, str), reason=m.obj_cast_color_ptr)
+def test_obj_cast_color_ptr():
+    with pytest.raises(RuntimeError) as excinfo:
+        m.obj_cast_color_ptr(m.color.red)
+    assert str(excinfo.value) == "Unable to cast native enum type to reference"
+
+
+def test_py_cast_color_handle():
+    for name, value in COLOR_MEMBERS:
+        assert m.py_cast_color_handle(m.color[name]) == value
+
+
+def test_exercise_import_or_getattr_leading_dot():
+    with pytest.raises(ValueError) as excinfo:
+        m.exercise_import_or_getattr(m, ".")
+    assert str(excinfo.value) == "Invalid fully-qualified name `.` (native_type_name)"
+
+
+def test_exercise_import_or_getattr_bad_top_level():
+    with pytest.raises(ImportError) as excinfo:
+        m.exercise_import_or_getattr(m, "NeVeRLaNd")
+    assert (
+        str(excinfo.value)
+        == "Failed to import top-level module `NeVeRLaNd` (native_type_name)"
+    )
+
+
+def test_exercise_import_or_getattr_dot_dot():
+    with pytest.raises(ValueError) as excinfo:
+        m.exercise_import_or_getattr(m, "enum..")
+    assert (
+        str(excinfo.value) == "Invalid fully-qualified name `enum..` (native_type_name)"
+    )
+
+
+def test_exercise_import_or_getattr_bad_enum_attr():
+    with pytest.raises(ImportError) as excinfo:
+        m.exercise_import_or_getattr(m, "enum.NoNeXiStInG")
+    lines = str(excinfo.value).splitlines()
+    assert len(lines) >= 5
+    assert (
+        lines[0]
+        == "Failed to import or getattr `NoNeXiStInG` from `enum` (native_type_name)"
+    )
+    assert lines[1] == "-------- getattr exception --------"
+    ix = lines.index("-------- import exception --------")
+    assert ix >= 3
+    assert len(lines) > ix + 0
+
+
+def test_native_enum_data_missing_finalize_error_message():
+    msg = m.native_enum_data_missing_finalize_error_message("Fake")
+    assert msg == 'pybind11::native_enum<...>("Fake", ...): MISSING .finalize()'
+
+
+@pytest.mark.parametrize(
+    "func", [m.native_enum_ctor_malformed_utf8, m.native_enum_value_malformed_utf8]
+)
+def test_native_enum_malformed_utf8(func):
+    if env.GRAALPY and func is m.native_enum_ctor_malformed_utf8:
+        pytest.skip("GraalPy does not raise UnicodeDecodeError")
+    malformed_utf8 = b"\x80"
+    with pytest.raises(UnicodeDecodeError):
+        func(malformed_utf8)
+
+
+def test_native_enum_double_finalize():
+    with pytest.raises(RuntimeError) as excinfo:
+        m.native_enum_double_finalize(m)
+    assert (
+        str(excinfo.value)
+        == 'pybind11::native_enum<...>("fake_native_enum_double_finalize"): DOUBLE finalize'
+    )
+
+
+def test_native_enum_value_after_finalize():
+    with pytest.raises(RuntimeError) as excinfo:
+        m.native_enum_value_after_finalize(m)
+    assert (
+        str(excinfo.value)
+        == 'pybind11::native_enum<...>("fake_native_enum_value_after_finalize"): value after finalize'
+    )
+
+
+def test_double_registration_native_enum():
+    with pytest.raises(RuntimeError) as excinfo:
+        m.double_registration_native_enum(m)
+    assert (
+        str(excinfo.value)
+        == 'pybind11::native_enum<...>("fake_double_registration_native_enum") is already registered!'
+    )
+
+
+def test_native_enum_name_clash():
+    m.fake_native_enum_name_clash = None
+    with pytest.raises(RuntimeError) as excinfo:
+        m.native_enum_name_clash(m)
+    assert (
+        str(excinfo.value)
+        == 'pybind11::native_enum<...>("fake_native_enum_name_clash"):'
+        " an object with that name is already defined"
+    )
+
+
+def test_native_enum_value_name_clash():
+    m.fake_native_enum_value_name_clash_x = None
+    with pytest.raises(RuntimeError) as excinfo:
+        m.native_enum_value_name_clash(m)
+    assert (
+        str(excinfo.value)
+        == 'pybind11::native_enum<...>("fake_native_enum_value_name_clash")'
+        '.value("fake_native_enum_value_name_clash_x"):'
+        " an object with that name is already defined"
+    )
+
+
+def test_double_registration_enum_before_native_enum():
+    with pytest.raises(RuntimeError) as excinfo:
+        m.double_registration_enum_before_native_enum(m)
+    assert (
+        str(excinfo.value)
+        == 'pybind11::native_enum<...>("fake_enum_first") is already registered'
+        " as a `pybind11::enum_` or `pybind11::class_`!"
+    )
+
+
+def test_double_registration_native_enum_before_enum():
+    with pytest.raises(RuntimeError) as excinfo:
+        m.double_registration_native_enum_before_enum(m)
+    assert (
+        str(excinfo.value)
+        == 'pybind11::enum_ "name_must_be_different_to_reach_desired_code_path"'
+        " is already registered as a pybind11::native_enum!"
+    )
+
+
+def test_native_enum_missing_finalize_failure():
+    if not isinstance(m.native_enum_missing_finalize_failure, str):
+        m.native_enum_missing_finalize_failure()
+        pytest.fail("Process termination expected.")


Reviews:
user: login: virtuald
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T03:29:31Z
user: login: rwgk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T03:45:42Z
user: login: virtuald
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T03:47:16Z
user: login: rwgk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T04:23:42Z
user: login: rwgk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T17:58:16Z
user: login: rwgk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T18:41:08Z
user: login: virtuald
body: I haven't tested it, but the tests you added seem comprehensive enough so I'm sure it's fine.

If it were possible to make this like the other types instead of having its own separate type map in internals, I'd definitely prefer that... it seems like this could introduce special cases that might accidentally introduce bugs? Looking at the `type_info` struct it of course has a lot of extra things that this wouldn't need, but I suspect maybe we could jam it in?

If you had looked at that previously and decided it wasn't possible, then I accept that and I'm not asking for additional research.

Edit: .. and even if you haven't done that research, it's probably not necessary. I like the new API change though.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T21:21:07Z
user: login: virtuald
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:10:18Z
user: login: rwgk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-16T07:00:32Z
user: login: timohl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T11:33:25Z
user: login: rwgk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-16T19:24:20Z
user: login: rwgk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-23T07:15:41Z
user: login: timohl
body: Thanks for your explanation.
I think the code looks good as is and makes sense for the purpose of creating Python bindings for C++ code.

Just for the documentation, in addition to the three requirements, it would be nice to explicitly list what Python stdlib Enums are working and which are not.
Then, newcomers would not have to study the Python docs or go by trial and error to find if `StrEnum` or `IntFlag` works.
Currently, `Enum` and `IntEnum` are mentioned in the docs.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T23:10:08Z
user: login: rwgk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T04:17:40Z
user: login: timohl
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T07:17:31Z
user: login: henryiii
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-25T03:19:17Z

Review comments:
diff_hunk: @@ -0,0 +1,192 @@
+#include <pybind11/native_enum.h>
+
+#include "pybind11_tests.h"
+
+#include <typeindex>
+
+namespace test_native_enum {
+
+// https://en.cppreference.com/w/cpp/language/enum
+
+// enum that takes 16 bits
+enum smallenum : std::int16_t { a, b, c };
+
+// color may be red (value 0), yellow (value 1), green (value 20), or blue (value 21)
+enum color { red, yellow, green = 20, blue };
+
+// altitude may be altitude::high or altitude::low
+enum class altitude : char {
+    high = 'h',
+    low = 'l', // trailing comma only allowed after CWG518
+};
+
+enum class export_values { exv0, exv1 };
+
+enum class member_doc { mem0, mem1, mem2 };
+
+struct class_with_enum {
+    enum class in_class { one, two };
+};
+
+// https://github.com/protocolbuffers/protobuf/blob/d70b5c5156858132decfdbae0a1103e6a5cb1345/src/google/protobuf/generated_enum_util.h#L52-L53
+template <typename T>
+struct is_proto_enum : std::false_type {};
+
+enum some_proto_enum : int { Zero, One };
+
+template <>
+struct is_proto_enum<some_proto_enum> : std::true_type {};
+
+} // namespace test_native_enum
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+PYBIND11_NAMESPACE_BEGIN(detail)
+
+template <typename ProtoEnumType>
+struct type_caster_enum_type_enabled<
+    ProtoEnumType,
+    detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> : std::false_type {
+};
+
+// https://github.com/pybind/pybind11_protobuf/blob/a50899c2eb604fc5f25deeb8901eff6231b8b3c0/pybind11_protobuf/enum_type_caster.h#L101-L105
+template <typename ProtoEnumType>
+struct type_caster<ProtoEnumType,
+                   detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> {
+    static handle
+    cast(const ProtoEnumType & /*src*/, return_value_policy /*policy*/, handle /*parent*/) {
+        return py::none();
+    }
+
+    bool load(handle /*src*/, bool /*convert*/) {
+        value = static_cast<ProtoEnumType>(0);
+        return true;
+    }
+
+    PYBIND11_TYPE_CASTER(ProtoEnumType, const_name<ProtoEnumType>());
+};
+
+PYBIND11_NAMESPACE_END(detail)
+PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
+
+TEST_SUBMODULE(native_enum, m) {
+    using namespace test_native_enum;
+
+    m += py::native_enum<smallenum>("smallenum", py::native_enum_kind::IntEnum)
path: tests/test_native_enum.cpp
user: login: virtuald
body: This API is pretty different than anything else in pybind11, since most everything else is in the form `py::TYPE<CTYPE> varname(scope, "NAME")`?

... I understand why, but the `+=` is really weird and is just not doing it for me. I asked ChatGPT what it thought such an API would look like, and it's basically the same thing I was thinking (except I started with `commit` instead of `finalize`, but I think `finalize` is better:

```C++
 auto my_enum = py::native_enum<MyEnum>(m, "MyEnum")
        .value("VALUE_A", MyEnum::VALUE_A)
        .value("VALUE_B", MyEnum::VALUE_B)
        .value("VALUE_C", MyEnum::VALUE_C);

    my_enum.finalize(); 
```
 
created_at: 2025-03-09T03:29:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,192 @@
+#include <pybind11/native_enum.h>
+
+#include "pybind11_tests.h"
+
+#include <typeindex>
+
+namespace test_native_enum {
+
+// https://en.cppreference.com/w/cpp/language/enum
+
+// enum that takes 16 bits
+enum smallenum : std::int16_t { a, b, c };
+
+// color may be red (value 0), yellow (value 1), green (value 20), or blue (value 21)
+enum color { red, yellow, green = 20, blue };
+
+// altitude may be altitude::high or altitude::low
+enum class altitude : char {
+    high = 'h',
+    low = 'l', // trailing comma only allowed after CWG518
+};
+
+enum class export_values { exv0, exv1 };
+
+enum class member_doc { mem0, mem1, mem2 };
+
+struct class_with_enum {
+    enum class in_class { one, two };
+};
+
+// https://github.com/protocolbuffers/protobuf/blob/d70b5c5156858132decfdbae0a1103e6a5cb1345/src/google/protobuf/generated_enum_util.h#L52-L53
+template <typename T>
+struct is_proto_enum : std::false_type {};
+
+enum some_proto_enum : int { Zero, One };
+
+template <>
+struct is_proto_enum<some_proto_enum> : std::true_type {};
+
+} // namespace test_native_enum
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+PYBIND11_NAMESPACE_BEGIN(detail)
+
+template <typename ProtoEnumType>
+struct type_caster_enum_type_enabled<
+    ProtoEnumType,
+    detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> : std::false_type {
+};
+
+// https://github.com/pybind/pybind11_protobuf/blob/a50899c2eb604fc5f25deeb8901eff6231b8b3c0/pybind11_protobuf/enum_type_caster.h#L101-L105
+template <typename ProtoEnumType>
+struct type_caster<ProtoEnumType,
+                   detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> {
+    static handle
+    cast(const ProtoEnumType & /*src*/, return_value_policy /*policy*/, handle /*parent*/) {
+        return py::none();
+    }
+
+    bool load(handle /*src*/, bool /*convert*/) {
+        value = static_cast<ProtoEnumType>(0);
+        return true;
+    }
+
+    PYBIND11_TYPE_CASTER(ProtoEnumType, const_name<ProtoEnumType>());
+};
+
+PYBIND11_NAMESPACE_END(detail)
+PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
+
+TEST_SUBMODULE(native_enum, m) {
+    using namespace test_native_enum;
+
+    m += py::native_enum<smallenum>("smallenum", py::native_enum_kind::IntEnum)
path: tests/test_native_enum.cpp
user: login: rwgk
body: Could you please share your ChatGPT prompt and the response? Usually it's giving constructive feedback (e.g. enumerates alternatives), it'd be great to see that.
created_at: 2025-03-09T03:45:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,192 @@
+#include <pybind11/native_enum.h>
+
+#include "pybind11_tests.h"
+
+#include <typeindex>
+
+namespace test_native_enum {
+
+// https://en.cppreference.com/w/cpp/language/enum
+
+// enum that takes 16 bits
+enum smallenum : std::int16_t { a, b, c };
+
+// color may be red (value 0), yellow (value 1), green (value 20), or blue (value 21)
+enum color { red, yellow, green = 20, blue };
+
+// altitude may be altitude::high or altitude::low
+enum class altitude : char {
+    high = 'h',
+    low = 'l', // trailing comma only allowed after CWG518
+};
+
+enum class export_values { exv0, exv1 };
+
+enum class member_doc { mem0, mem1, mem2 };
+
+struct class_with_enum {
+    enum class in_class { one, two };
+};
+
+// https://github.com/protocolbuffers/protobuf/blob/d70b5c5156858132decfdbae0a1103e6a5cb1345/src/google/protobuf/generated_enum_util.h#L52-L53
+template <typename T>
+struct is_proto_enum : std::false_type {};
+
+enum some_proto_enum : int { Zero, One };
+
+template <>
+struct is_proto_enum<some_proto_enum> : std::true_type {};
+
+} // namespace test_native_enum
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+PYBIND11_NAMESPACE_BEGIN(detail)
+
+template <typename ProtoEnumType>
+struct type_caster_enum_type_enabled<
+    ProtoEnumType,
+    detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> : std::false_type {
+};
+
+// https://github.com/pybind/pybind11_protobuf/blob/a50899c2eb604fc5f25deeb8901eff6231b8b3c0/pybind11_protobuf/enum_type_caster.h#L101-L105
+template <typename ProtoEnumType>
+struct type_caster<ProtoEnumType,
+                   detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> {
+    static handle
+    cast(const ProtoEnumType & /*src*/, return_value_policy /*policy*/, handle /*parent*/) {
+        return py::none();
+    }
+
+    bool load(handle /*src*/, bool /*convert*/) {
+        value = static_cast<ProtoEnumType>(0);
+        return true;
+    }
+
+    PYBIND11_TYPE_CASTER(ProtoEnumType, const_name<ProtoEnumType>());
+};
+
+PYBIND11_NAMESPACE_END(detail)
+PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
+
+TEST_SUBMODULE(native_enum, m) {
+    using namespace test_native_enum;
+
+    m += py::native_enum<smallenum>("smallenum", py::native_enum_kind::IntEnum)
path: tests/test_native_enum.cpp
user: login: virtuald
body: It didn't because of the way I asked it, but sure: https://chatgpt.com/share/67cd0f1f-6df8-800f-b092-b459cea53485
created_at: 2025-03-09T03:47:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,192 @@
+#include <pybind11/native_enum.h>
+
+#include "pybind11_tests.h"
+
+#include <typeindex>
+
+namespace test_native_enum {
+
+// https://en.cppreference.com/w/cpp/language/enum
+
+// enum that takes 16 bits
+enum smallenum : std::int16_t { a, b, c };
+
+// color may be red (value 0), yellow (value 1), green (value 20), or blue (value 21)
+enum color { red, yellow, green = 20, blue };
+
+// altitude may be altitude::high or altitude::low
+enum class altitude : char {
+    high = 'h',
+    low = 'l', // trailing comma only allowed after CWG518
+};
+
+enum class export_values { exv0, exv1 };
+
+enum class member_doc { mem0, mem1, mem2 };
+
+struct class_with_enum {
+    enum class in_class { one, two };
+};
+
+// https://github.com/protocolbuffers/protobuf/blob/d70b5c5156858132decfdbae0a1103e6a5cb1345/src/google/protobuf/generated_enum_util.h#L52-L53
+template <typename T>
+struct is_proto_enum : std::false_type {};
+
+enum some_proto_enum : int { Zero, One };
+
+template <>
+struct is_proto_enum<some_proto_enum> : std::true_type {};
+
+} // namespace test_native_enum
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+PYBIND11_NAMESPACE_BEGIN(detail)
+
+template <typename ProtoEnumType>
+struct type_caster_enum_type_enabled<
+    ProtoEnumType,
+    detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> : std::false_type {
+};
+
+// https://github.com/pybind/pybind11_protobuf/blob/a50899c2eb604fc5f25deeb8901eff6231b8b3c0/pybind11_protobuf/enum_type_caster.h#L101-L105
+template <typename ProtoEnumType>
+struct type_caster<ProtoEnumType,
+                   detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> {
+    static handle
+    cast(const ProtoEnumType & /*src*/, return_value_policy /*policy*/, handle /*parent*/) {
+        return py::none();
+    }
+
+    bool load(handle /*src*/, bool /*convert*/) {
+        value = static_cast<ProtoEnumType>(0);
+        return true;
+    }
+
+    PYBIND11_TYPE_CASTER(ProtoEnumType, const_name<ProtoEnumType>());
+};
+
+PYBIND11_NAMESPACE_END(detail)
+PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
+
+TEST_SUBMODULE(native_enum, m) {
+    using namespace test_native_enum;
+
+    m += py::native_enum<smallenum>("smallenum", py::native_enum_kind::IntEnum)
path: tests/test_native_enum.cpp
user: login: rwgk
body: Very useful, thanks!

Here is my continuation:

https://chatgpt.com/share/67cd1767-d63c-8008-acb8-4c50cf739e85

It's explaining three options to preserve the safety net mechanism that I have in place. I'll work through that asap.
created_at: 2025-03-09T04:23:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,192 @@
+#include <pybind11/native_enum.h>
+
+#include "pybind11_tests.h"
+
+#include <typeindex>
+
+namespace test_native_enum {
+
+// https://en.cppreference.com/w/cpp/language/enum
+
+// enum that takes 16 bits
+enum smallenum : std::int16_t { a, b, c };
+
+// color may be red (value 0), yellow (value 1), green (value 20), or blue (value 21)
+enum color { red, yellow, green = 20, blue };
+
+// altitude may be altitude::high or altitude::low
+enum class altitude : char {
+    high = 'h',
+    low = 'l', // trailing comma only allowed after CWG518
+};
+
+enum class export_values { exv0, exv1 };
+
+enum class member_doc { mem0, mem1, mem2 };
+
+struct class_with_enum {
+    enum class in_class { one, two };
+};
+
+// https://github.com/protocolbuffers/protobuf/blob/d70b5c5156858132decfdbae0a1103e6a5cb1345/src/google/protobuf/generated_enum_util.h#L52-L53
+template <typename T>
+struct is_proto_enum : std::false_type {};
+
+enum some_proto_enum : int { Zero, One };
+
+template <>
+struct is_proto_enum<some_proto_enum> : std::true_type {};
+
+} // namespace test_native_enum
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+PYBIND11_NAMESPACE_BEGIN(detail)
+
+template <typename ProtoEnumType>
+struct type_caster_enum_type_enabled<
+    ProtoEnumType,
+    detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> : std::false_type {
+};
+
+// https://github.com/pybind/pybind11_protobuf/blob/a50899c2eb604fc5f25deeb8901eff6231b8b3c0/pybind11_protobuf/enum_type_caster.h#L101-L105
+template <typename ProtoEnumType>
+struct type_caster<ProtoEnumType,
+                   detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> {
+    static handle
+    cast(const ProtoEnumType & /*src*/, return_value_policy /*policy*/, handle /*parent*/) {
+        return py::none();
+    }
+
+    bool load(handle /*src*/, bool /*convert*/) {
+        value = static_cast<ProtoEnumType>(0);
+        return true;
+    }
+
+    PYBIND11_TYPE_CASTER(ProtoEnumType, const_name<ProtoEnumType>());
+};
+
+PYBIND11_NAMESPACE_END(detail)
+PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
+
+TEST_SUBMODULE(native_enum, m) {
+    using namespace test_native_enum;
+
+    m += py::native_enum<smallenum>("smallenum", py::native_enum_kind::IntEnum)
path: tests/test_native_enum.cpp
user: login: rwgk
body: Ugh, I spent half an hour on a really good conversation with ChatGPT, coming to a great conclusion, but I was accidentally not logged in (wrong profile), I tried to login, that failed, and now the convo is lost 😭.

I'll try to see if I can redo it with the same level of authenticity.
created_at: 2025-03-09T17:58:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,192 @@
+#include <pybind11/native_enum.h>
+
+#include "pybind11_tests.h"
+
+#include <typeindex>
+
+namespace test_native_enum {
+
+// https://en.cppreference.com/w/cpp/language/enum
+
+// enum that takes 16 bits
+enum smallenum : std::int16_t { a, b, c };
+
+// color may be red (value 0), yellow (value 1), green (value 20), or blue (value 21)
+enum color { red, yellow, green = 20, blue };
+
+// altitude may be altitude::high or altitude::low
+enum class altitude : char {
+    high = 'h',
+    low = 'l', // trailing comma only allowed after CWG518
+};
+
+enum class export_values { exv0, exv1 };
+
+enum class member_doc { mem0, mem1, mem2 };
+
+struct class_with_enum {
+    enum class in_class { one, two };
+};
+
+// https://github.com/protocolbuffers/protobuf/blob/d70b5c5156858132decfdbae0a1103e6a5cb1345/src/google/protobuf/generated_enum_util.h#L52-L53
+template <typename T>
+struct is_proto_enum : std::false_type {};
+
+enum some_proto_enum : int { Zero, One };
+
+template <>
+struct is_proto_enum<some_proto_enum> : std::true_type {};
+
+} // namespace test_native_enum
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+PYBIND11_NAMESPACE_BEGIN(detail)
+
+template <typename ProtoEnumType>
+struct type_caster_enum_type_enabled<
+    ProtoEnumType,
+    detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> : std::false_type {
+};
+
+// https://github.com/pybind/pybind11_protobuf/blob/a50899c2eb604fc5f25deeb8901eff6231b8b3c0/pybind11_protobuf/enum_type_caster.h#L101-L105
+template <typename ProtoEnumType>
+struct type_caster<ProtoEnumType,
+                   detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> {
+    static handle
+    cast(const ProtoEnumType & /*src*/, return_value_policy /*policy*/, handle /*parent*/) {
+        return py::none();
+    }
+
+    bool load(handle /*src*/, bool /*convert*/) {
+        value = static_cast<ProtoEnumType>(0);
+        return true;
+    }
+
+    PYBIND11_TYPE_CASTER(ProtoEnumType, const_name<ProtoEnumType>());
+};
+
+PYBIND11_NAMESPACE_END(detail)
+PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
+
+TEST_SUBMODULE(native_enum, m) {
+    using namespace test_native_enum;
+
+    m += py::native_enum<smallenum>("smallenum", py::native_enum_kind::IntEnum)
path: tests/test_native_enum.cpp
user: login: rwgk
body: It turns out there is no way to get back the original conversation. ChatGPT took a different turn, maybe because my prompt wasn't exactly as before.

But, I think the conclusion is now even better!

Here is the new convo:

* https://chatgpt.com/share/67cddfe0-dfec-8008-a00f-a33468ac8ab7

And here the conclusion for easy reference:

```C++
py::native_enum<MyEnum>(m, "MyEnum")
    .value("VALUE_A", MyEnum::VALUE_A)
    .value("VALUE_B", MyEnum::VALUE_B)
    .value("VALUE_C", MyEnum::VALUE_C)
    .finalize();
```

I'll take a stab at that. I like it a lot better myself.
created_at: 2025-03-09T18:41:08Z
author_association: COLLABORATOR
diff_hunk: @@ -494,69 +494,37 @@ The binding code for this example looks as follows:
         .def_readwrite("type", &Pet::type)
         .def_readwrite("attr", &Pet::attr);
 
-    py::enum_<Pet::Kind>(pet, "Kind")
+    py::native_enum<Pet::Kind>(pet, "Kind")
         .value("Dog", Pet::Kind::Dog)
         .value("Cat", Pet::Kind::Cat)
-        .export_values();
+        .export_values()
+        .finalize();
 
     py::class_<Pet::Attributes>(pet, "Attributes")
         .def(py::init<>())
         .def_readwrite("age", &Pet::Attributes::age);
 
 
-To ensure that the nested types ``Kind`` and ``Attributes`` are created within the scope of ``Pet``, the
-``pet`` ``py::class_`` instance must be supplied to the :class:`enum_` and ``py::class_``
-constructor. The :func:`enum_::export_values` function exports the enum entries
-into the parent scope, which should be skipped for newer C++11-style strongly
-typed enums.
-
-.. code-block:: pycon
-
-    >>> p = Pet("Lucy", Pet.Cat)
-    >>> p.type
-    Kind.Cat
-    >>> int(p.type)
-    1L
-
-The entries defined by the enumeration type are exposed in the ``__members__`` property:
-
-.. code-block:: pycon
-
-    >>> Pet.Kind.__members__
-    {'Dog': Kind.Dog, 'Cat': Kind.Cat}
-
-The ``name`` property returns the name of the enum value as a unicode string.
-
-.. note::
-
-    It is also possible to use ``str(enum)``, however these accomplish different
-    goals. The following shows how these two approaches differ.
-
-    .. code-block:: pycon
-
-        >>> p = Pet("Lucy", Pet.Cat)
-        >>> pet_type = p.type
-        >>> pet_type
-        Pet.Cat
-        >>> str(pet_type)
-        'Pet.Cat'
-        >>> pet_type.name
-        'Cat'
-
-.. note::
-
-    When the special tag ``py::arithmetic()`` is specified to the ``enum_``
-    constructor, pybind11 creates an enumeration that also supports rudimentary
-    arithmetic and bit-level operations like comparisons, and, or, xor, negation,
-    etc.
-
-    .. code-block:: cpp
-
-        py::enum_<Pet::Kind>(pet, "Kind", py::arithmetic())
-           ...
-
-    By default, these are omitted to conserve space.
-
-.. warning::
-
-    Contrary to Python customs, enum values from the wrappers should not be compared using ``is``, but with ``==`` (see `#1177 <https://github.com/pybind/pybind11/issues/1177>`_ for background).
path: docs/classes.rst
user: login: virtuald
body: Hm. Is this warning captured anywhere else? It doesn't appear to be, but if we're keeping the old enum stuff it probably should be somewhere? Maybe this (and other things that were removed) could be added in a separate note?

```
.. note:: The deprecated `py::enum_` differs from standard python enums in the following ways:  ... 

```
created_at: 2025-03-10T16:01:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -494,69 +494,37 @@ The binding code for this example looks as follows:
         .def_readwrite("type", &Pet::type)
         .def_readwrite("attr", &Pet::attr);
 
-    py::enum_<Pet::Kind>(pet, "Kind")
+    py::native_enum<Pet::Kind>(pet, "Kind")
         .value("Dog", Pet::Kind::Dog)
         .value("Cat", Pet::Kind::Cat)
-        .export_values();
+        .export_values()
+        .finalize();
 
     py::class_<Pet::Attributes>(pet, "Attributes")
         .def(py::init<>())
         .def_readwrite("age", &Pet::Attributes::age);
 
 
-To ensure that the nested types ``Kind`` and ``Attributes`` are created within the scope of ``Pet``, the
-``pet`` ``py::class_`` instance must be supplied to the :class:`enum_` and ``py::class_``
-constructor. The :func:`enum_::export_values` function exports the enum entries
-into the parent scope, which should be skipped for newer C++11-style strongly
-typed enums.
-
-.. code-block:: pycon
-
-    >>> p = Pet("Lucy", Pet.Cat)
-    >>> p.type
-    Kind.Cat
-    >>> int(p.type)
-    1L
-
-The entries defined by the enumeration type are exposed in the ``__members__`` property:
-
-.. code-block:: pycon
-
-    >>> Pet.Kind.__members__
-    {'Dog': Kind.Dog, 'Cat': Kind.Cat}
-
-The ``name`` property returns the name of the enum value as a unicode string.
-
-.. note::
-
-    It is also possible to use ``str(enum)``, however these accomplish different
-    goals. The following shows how these two approaches differ.
-
-    .. code-block:: pycon
-
-        >>> p = Pet("Lucy", Pet.Cat)
-        >>> pet_type = p.type
-        >>> pet_type
-        Pet.Cat
-        >>> str(pet_type)
-        'Pet.Cat'
-        >>> pet_type.name
-        'Cat'
-
-.. note::
-
-    When the special tag ``py::arithmetic()`` is specified to the ``enum_``
-    constructor, pybind11 creates an enumeration that also supports rudimentary
-    arithmetic and bit-level operations like comparisons, and, or, xor, negation,
-    etc.
-
-    .. code-block:: cpp
-
-        py::enum_<Pet::Kind>(pet, "Kind", py::arithmetic())
-           ...
-
-    By default, these are omitted to conserve space.
-
-.. warning::
-
-    Contrary to Python customs, enum values from the wrappers should not be compared using ``is``, but with ``==`` (see `#1177 <https://github.com/pybind/pybind11/issues/1177>`_ for background).
path: docs/classes.rst
user: login: rwgk
body: The `py::enum_` documentation is now here:

https://pybind11--5555.org.readthedocs.build/en/5555/advanced/deprecated.html#deprecated-enum
created_at: 2025-03-16T07:00:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,63 @@
+// Copyright (c) 2022-2025 The pybind Community.
+// All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+#pragma once
+
+#include "detail/common.h"
+#include "detail/native_enum_data.h"
+#include "detail/type_caster_base.h"
+#include "cast.h"
+
+#include <cassert>
+#include <limits>
+#include <type_traits>
+#include <typeindex>
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+
+/// Conversions between Python's native (stdlib) enum types and C++ enums.
+template <typename Type>
+class native_enum : public detail::native_enum_data {
+public:
+    using Underlying = typename std::underlying_type<Type>::type;
+
+    native_enum(const object &parent_scope, const char *name, enum_kind kind = enum_kind::Enum)
path: include/pybind11/native_enum.h
user: login: timohl
body: Would it maybe make future extensions to StrEnum or other enum kinds easier if `enum_kind kind` would be a template parameter instead of a constructor argument?

Currently, `kind` is only used in `enum_kind_to_string`, but I could imagine, that other enum kinds would require a different specialization of `native_enum` or `native_enum_data`, e.g. to pass or store the string representations.

I am thinking of something like this:
```cpp
template <typename Type, enum_kind kind = enum_kind::Enum>
class native_enum : public detail::native_enum_data<kind> {
public:
    using Underlying = typename std::underlying_type<Type>::type;

    native_enum(const object &parent_scope, const char *name)
...
```
and
```cpp
py::native_enum<Pet::Kind, py::enum_kind::IntEnum>(pet, "Kind")
```

While I do not have specific implementations of the other Enum kinds in mind, I think this would offer more flexibility later while being only a small change now.
But please tell me if I am overlooking any downsides.
created_at: 2025-03-16T11:11:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+// Copyright (c) 2022-2025 The pybind Community.
+// All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+#pragma once
+
+#include "detail/common.h"
+#include "detail/native_enum_data.h"
+#include "detail/type_caster_base.h"
+#include "cast.h"
+
+#include <cassert>
+#include <limits>
+#include <type_traits>
+#include <typeindex>
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+
+/// Conversions between Python's native (stdlib) enum types and C++ enums.
+template <typename Type>
+class native_enum : public detail::native_enum_data {
+public:
+    using Underlying = typename std::underlying_type<Type>::type;
+
+    native_enum(const object &parent_scope, const char *name, enum_kind kind = enum_kind::Enum)
path: include/pybind11/native_enum.h
user: login: rwgk
body: @timohl Thanks for asking that question.

After thinking about it for a few minutes, I realized (face-palm level) that `py::enum_kind` is going completely in the wrong direction. I eliminated it now: commit 8efb9bf81a8d53e5c9b7ec9b9bd690bbdad91d54

I have 3 `WIP-TODO` in that commit.

I also want to try `StrEnum` again, maybe we can accommodate that via something like

```C++
        py::native_enum<greek>(m, "greek", "enum.StrEnum")
            .value("Alpha", greek::Alpha, "alpha")
            .value("Omega", greek::Omega, "omega")
            .finalize();
```

?

This will have to wait until next weekend, though.
created_at: 2025-03-16T19:24:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,63 @@
+// Copyright (c) 2022-2025 The pybind Community.
+// All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+#pragma once
+
+#include "detail/common.h"
+#include "detail/native_enum_data.h"
+#include "detail/type_caster_base.h"
+#include "cast.h"
+
+#include <cassert>
+#include <limits>
+#include <type_traits>
+#include <typeindex>
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+
+/// Conversions between Python's native (stdlib) enum types and C++ enums.
+template <typename Type>
+class native_enum : public detail::native_enum_data {
+public:
+    using Underlying = typename std::underlying_type<Type>::type;
+
+    native_enum(const object &parent_scope, const char *name, enum_kind kind = enum_kind::Enum)
path: include/pybind11/native_enum.h
user: login: rwgk
body: Regarding `StrEnum`, [I played around for an hour or so](https://github.com/pybind/pybind11/pull/5555/commits/0e46fa08d0427241647949b7248f4e0b7c7813bf), but then decided this isn't worth extra code. See the commit message.
created_at: 2025-03-23T07:15:41Z
author_association: COLLABORATOR
diff_hunk: @@ -487,76 +489,97 @@ The binding code for this example looks as follows:
 
 .. code-block:: cpp
 
+    #include <pybind11/native_enum.h> // Not already included with pybind11.h
+
     py::class_<Pet> pet(m, "Pet");
 
     pet.def(py::init<const std::string &, Pet::Kind>())
         .def_readwrite("name", &Pet::name)
         .def_readwrite("type", &Pet::type)
         .def_readwrite("attr", &Pet::attr);
 
-    py::enum_<Pet::Kind>(pet, "Kind")
+    py::native_enum<Pet::Kind>(pet, "Kind")
         .value("Dog", Pet::Kind::Dog)
         .value("Cat", Pet::Kind::Cat)
-        .export_values();
+        .export_values()
+        .finalize();
 
     py::class_<Pet::Attributes>(pet, "Attributes")
         .def(py::init<>())
         .def_readwrite("age", &Pet::Attributes::age);
 
 
-To ensure that the nested types ``Kind`` and ``Attributes`` are created within the scope of ``Pet``, the
-``pet`` ``py::class_`` instance must be supplied to the :class:`enum_` and ``py::class_``
-constructor. The :func:`enum_::export_values` function exports the enum entries
-into the parent scope, which should be skipped for newer C++11-style strongly
-typed enums.
+To ensure that the nested types ``Kind`` and ``Attributes`` are created
+within the scope of ``Pet``, the ``pet`` ``py::class_`` instance must be
+supplied to the ``py::native_enum`` and ``py::class_`` constructors. The
+``.export_values()`` function is available for exporting the enum entries
+into the parent scope, if desired.
 
-.. code-block:: pycon
+``py::native_enum`` was introduced with pybind11v3. It binds C++ enum types
+to native Python enum types, typically types in Python's `stdlib enum`_ module,
+which are `PEP 435 compatible <https://peps.python.org/pep-0435/>`_.
+This is the recommended way to bind C++ enums.
+The older ``py::enum_`` is not PEP 435 compatible
+(see `issue #2332 <https://github.com/pybind/pybind11/issues/2332>`_)
+but remains supported indefinitely for backward compatibility.
+New bindings should prefer ``py::native_enum``.
 
-    >>> p = Pet("Lucy", Pet.Cat)
-    >>> p.type
-    Kind.Cat
-    >>> int(p.type)
-    1L
+.. note::
 
-The entries defined by the enumeration type are exposed in the ``__members__`` property:
+    The deprecated ``py::enum_`` is :ref:`documented here <deprecated_enum>`.
 
-.. code-block:: pycon
+The ``.finalize()`` call above is needed because Python's native enums
+cannot be built incrementally — all name/value pairs need to be passed at
+once. To achieve this, ``py::native_enum`` acts as a buffer to collect the
+name/value pairs. The ``.finalize()`` call uses the accumulated name/value
+pairs to build the arguments for constructing a native Python enum type.
 
-    >>> Pet.Kind.__members__
-    {'Dog': Kind.Dog, 'Cat': Kind.Cat}
+The ``py::native_enum`` constructor supports a third optional
+``native_type_name`` string argument, with default value ``"enum.Enum"``.
+Other types can be specified like this:
 
-The ``name`` property returns the name of the enum value as a unicode string.
+.. code-block:: cpp
 
-.. note::
+    py::native_enum<Pet::Kind>(pet, "Kind", "enum.IntEnum")
 
-    It is also possible to use ``str(enum)``, however these accomplish different
-    goals. The following shows how these two approaches differ.
+Any fully-qualified Python name can be specified. The only requirement is
+that the named type is similar to enum.Enum_ in these ways:
 
-    .. code-block:: pycon
+* Has a constructor similar to that of enum.Enum_::
path: docs/classes.rst
user: login: timohl
body: This creates a link to:
[docs.python.org/3/library/enum.html#enum.Enum](https://docs.python.org/3/library/enum.html#enum.Enum)
I think a link to this section should be more relevant to the functional style constructor:
[docs.python.org/3/howto/enum.html#functional-api](https://docs.python.org/3/howto/enum.html#functional-api)
created_at: 2025-03-23T22:40:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -487,76 +489,97 @@ The binding code for this example looks as follows:
 
 .. code-block:: cpp
 
+    #include <pybind11/native_enum.h> // Not already included with pybind11.h
+
     py::class_<Pet> pet(m, "Pet");
 
     pet.def(py::init<const std::string &, Pet::Kind>())
         .def_readwrite("name", &Pet::name)
         .def_readwrite("type", &Pet::type)
         .def_readwrite("attr", &Pet::attr);
 
-    py::enum_<Pet::Kind>(pet, "Kind")
+    py::native_enum<Pet::Kind>(pet, "Kind")
         .value("Dog", Pet::Kind::Dog)
         .value("Cat", Pet::Kind::Cat)
-        .export_values();
+        .export_values()
+        .finalize();
 
     py::class_<Pet::Attributes>(pet, "Attributes")
         .def(py::init<>())
         .def_readwrite("age", &Pet::Attributes::age);
 
 
-To ensure that the nested types ``Kind`` and ``Attributes`` are created within the scope of ``Pet``, the
-``pet`` ``py::class_`` instance must be supplied to the :class:`enum_` and ``py::class_``
-constructor. The :func:`enum_::export_values` function exports the enum entries
-into the parent scope, which should be skipped for newer C++11-style strongly
-typed enums.
+To ensure that the nested types ``Kind`` and ``Attributes`` are created
+within the scope of ``Pet``, the ``pet`` ``py::class_`` instance must be
+supplied to the ``py::native_enum`` and ``py::class_`` constructors. The
+``.export_values()`` function is available for exporting the enum entries
+into the parent scope, if desired.
 
-.. code-block:: pycon
+``py::native_enum`` was introduced with pybind11v3. It binds C++ enum types
+to native Python enum types, typically types in Python's `stdlib enum`_ module,
+which are `PEP 435 compatible <https://peps.python.org/pep-0435/>`_.
+This is the recommended way to bind C++ enums.
+The older ``py::enum_`` is not PEP 435 compatible
+(see `issue #2332 <https://github.com/pybind/pybind11/issues/2332>`_)
+but remains supported indefinitely for backward compatibility.
+New bindings should prefer ``py::native_enum``.
 
-    >>> p = Pet("Lucy", Pet.Cat)
-    >>> p.type
-    Kind.Cat
-    >>> int(p.type)
-    1L
+.. note::
 
-The entries defined by the enumeration type are exposed in the ``__members__`` property:
+    The deprecated ``py::enum_`` is :ref:`documented here <deprecated_enum>`.
 
-.. code-block:: pycon
+The ``.finalize()`` call above is needed because Python's native enums
+cannot be built incrementally — all name/value pairs need to be passed at
+once. To achieve this, ``py::native_enum`` acts as a buffer to collect the
+name/value pairs. The ``.finalize()`` call uses the accumulated name/value
+pairs to build the arguments for constructing a native Python enum type.
 
-    >>> Pet.Kind.__members__
-    {'Dog': Kind.Dog, 'Cat': Kind.Cat}
+The ``py::native_enum`` constructor supports a third optional
+``native_type_name`` string argument, with default value ``"enum.Enum"``.
+Other types can be specified like this:
 
-The ``name`` property returns the name of the enum value as a unicode string.
+.. code-block:: cpp
 
-.. note::
+    py::native_enum<Pet::Kind>(pet, "Kind", "enum.IntEnum")
 
-    It is also possible to use ``str(enum)``, however these accomplish different
-    goals. The following shows how these two approaches differ.
+Any fully-qualified Python name can be specified. The only requirement is
+that the named type is similar to enum.Enum_ in these ways:
 
-    .. code-block:: pycon
+* Has a constructor similar to that of enum.Enum_::
path: docs/classes.rst
user: login: rwgk
body: Done: commit cbb7494549d8c648c0f1d4394972fd2a2b909a37
created_at: 2025-03-24T04:17:40Z
author_association: COLLABORATOR

PR comments:
user: login: rwgk
created_at: 2025-03-09T20:27:26Z
body: @virtuald FYI, I made the API change, but still need to change the error messages (they still direct users to use `+=` instead of `.finalize()`).
author_association: COLLABORATOR
user: login: rwgk
created_at: 2025-03-09T22:11:03Z
body: Assuming GitHub Actions are passing now, this is now feature complete; the error message is updated.

Things I still want to do:

* Add tests for `.finalize().value(...)` and `.finalize().finalize()` error handling.

* See if there is a reasonably easy way to make some `native_enum_data` member functions inaccessible through the public API (I need them to be accessible for testing).

author_association: COLLABORATOR
user: login: rwgk
created_at: 2025-03-09T22:14:45Z
body: > Looking at the `type_info` struct it of course has a lot of extra things that this wouldn't need, but I suspect maybe we could jam it in?

I very much rather not do that. I have a lot of bad experiences trying to jam things together.

We have a golden opportunity right now to change the internals struct, to keep the implementation clean, which is why I'm sinking my time into this PR now.
author_association: COLLABORATOR
user: login: rwgk
created_at: 2025-03-10T05:19:03Z
body: All done with
* error handling update,
* public API cleanup,
* PR description update,
* docs/classes.rst update.

@virtuald Could you please take another look, especially at the docs/classes.rst update?

@timohl It would be awesome if you could look, too.

For easy access: https://pybind11--5555.org.readthedocs.build/en/5555/classes.html#enumerations
author_association: COLLABORATOR
user: login: rwgk
created_at: 2025-03-10T15:23:46Z
body: > All done with
> 
> * error handling update,
> * public API cleanup,
> * PR description update,
> * docs/classes.rst update.
> 
> @virtuald Could you please take another look, especially at the docs/classes.rst update?
> 
> @timohl It would be awesome if you could look, too.
> 
> For easy access: https://pybind11--5555.org.readthedocs.build/en/5555/classes.html#enumerations

It crossed my mind, I totally forgot about adding this to the documentation (only the PR description there is interesting):

* https://github.com/google/pybind11clif/pull/30118

I'll do that asap. It will not change the code, only the documentation...

**unless we decide we want `py::native_enum_kind::IntEnum` as the default?**
author_association: COLLABORATOR
user: login: virtuald
created_at: 2025-03-10T16:10:04Z
body: I haven't thought super hard about this, but I don't think IntEnum should be default.
author_association: CONTRIBUTOR
user: login: rwgk
created_at: 2025-03-10T18:04:12Z
body: > I haven't thought super hard about this, but I don't think IntEnum should be default.

With this PR as-is, we always need to specify `py::native_enum_kind` (I just updated the example in the PR description; the documentation still needs to be updated):

```
py::native_enum<my_chars>("smallenum", py::native_enum_kind::Enum) ...
```
```
py::native_enum<my_ints>("smallenum", py::native_enum_kind::IntEnum) ...
```

`py::native_enum_kind` has only two choices, which gives us three API design options:

* No default (this PR as-is).
* `py::native_enum_kind::Enum` as the default.
* `py::native_enum_kind::IntEnum` as the default.

Back when I created the pybind11clif PR, I thought having no default was best because it forces people to actively think about the choice (copy-paste-think vs copy-paste-move-on).

No default is still my preference. It's slightly inconvenient when writing enum bindings since you have to pause and make a decision. But then again, if there's a default, there’s an unknown (but non-zero) chance that someone might wind up with the wrong option and only realize it when it’s too late — e.g. after a release has already been made.

Regarding the "unknown": Do others feel differently? For example, is it the case that in 99% of use cases, `Enum` or `IntEnum` would be the better default?
author_association: COLLABORATOR
user: login: henryiii
created_at: 2025-03-10T19:53:35Z
body: Haven't looked at this closely, but I prefer no default or `Enum`, making `IntEnum` the default seems really weird, since that's not the case in Python, and generally you want the more limited API the default - IntEnum just adds extra API a user might not want. So making Enum the default seems most reasonable, with no default being a close second. What about StrEnum (Python 3.11+)?

Also not very fond of making `enum_` now permanently be spelled `native_enum`, but don't have a better solution, other than maybe we can remove `enum_` in 4.0 in a few years and then restore `enum_` as an alias to `native_enum` in 4.1 or so. Maybe we could even rename `enum_` to `classic_enum` with `enum_` just being an alias to that. With that, does `native_enum_kind` maybe make more sense as `enum_kind`? Since there isn't one for the old enums? Also, "native" seems a little odd, it's not clear to me that's related to Python. What about `std_enum`? Though that also isn't really clearly the Python stdlib. Historians might like `enum34`, as that's what the old backport package was called. :) I'm fine with `native_enum`, though.


How different is the official enum API to the one we provided?
author_association: COLLABORATOR
user: login: rwgk
created_at: 2025-03-11T03:31:26Z
body: @virtuald wrote:

> It doesn't appear to be, but if we're keeping the old enum stuff it probably should be somewhere? 

OK, I'll put back some info about `enum_`.

@henry wrote:

>So making Enum the default seems most reasonable

OK, I'll do that.

> What about StrEnum (Python 3.11+)?

I totally missed that, I'll take a look, thanks.

> Also not very fond of making `enum_` now permanently be spelled `native_enum`

Yep, but breaking a bunch of things just to insist on a certain name ... it really doesn't seem worth anyone's time to me.

> How different is the official enum API to the one we provided?

I didn't look systematically. Based on glancing through #2332 I decided a systematic comparison is too messy.

I'll probably only get back here next weekend. It's turning into more work than I anticipated. I appreciate the feedback.
author_association: COLLABORATOR
user: login: henryiii
created_at: 2025-03-12T14:52:41Z
body: You don't need to add `StrEnum`, especially in this PR, I just think it would be good to make sure the design would allow it in the future, though. It's another specialization, like IntEnum.
author_association: COLLABORATOR
user: login: rwgk
created_at: 2025-03-16T02:40:56Z
body: I'm done with code updates suggested here:

* commit 11f597bb50398c43a0c0a395e8bf401a0407e64e — Rename `py::native_enum_kind` → `py::enum_kind` as suggested by @henryiii

* commit 6180ebc60a683f36e017d50997bbc0eda3b6fc14 — Make `enum_kind::Enum` the default kind.

I also tried this without first thinking too much:

* commit 190f99c49bb1a7d87361d88cb73ac0e249f8c769 — `StrEnum` experiment. I gave up after realizing that [Python's `StrEnum` does not map naturally to C++](https://chatgpt.com/share/67d5e965-ccb0-8008-95b7-0df2502309b3).

However, I kept half of the experiment [when reverting the `StrEnum` addition](df39090d3804a429e681a8d2e0b96940f51ada88), so that we now have `enum_kind kind` instead of `bool use_int_enum` in `detail::native_enum_data`. I believe that might work better in the future.
author_association: COLLABORATOR
user: login: rwgk
created_at: 2025-03-16T02:52:35Z
body: I forgot to add: I'm still working on the documentation.
author_association: COLLABORATOR
user: login: rwgk
created_at: 2025-03-16T07:01:54Z
body: @henryiii @virtuald I'm done working through the suggested changes, additional documentation, and cleanup. Could you please take another look?
author_association: COLLABORATOR
user: login: timohl
created_at: 2025-03-16T10:45:19Z
body: Sorry for the delay (was a sick for a couple of days) but I will take a look now as well.

Regarding support for other Python Enum classes, I wanted to make you aware, that Python added several kinds of Enums and Enum support functions ([see docs](https://docs.python.org/3/library/enum.html#module-contents)):
```
Added in version 3.6: Flag, IntFlag, auto

Added in version 3.11: StrEnum, EnumCheck, ReprEnum, FlagBoundary, property, member, nonmember, global_enum, show_flag_values

Added in version 3.13: EnumDict
```


I think the easiest enum kinds to translate to C++ would be `Flag` and `IntFlag`


> [Flag](https://docs.python.org/3/library/enum.html#enum.Flag)
> 
> Base class for creating enumerated constants that can be combined using the bitwise operations without losing their [Flag](https://docs.python.org/3/library/enum.html#enum.Flag) membership.
> 
> [IntFlag](https://docs.python.org/3/library/enum.html#enum.IntFlag)
> 
> Base class for creating enumerated constants that can be combined using the bitwise operators without losing their [IntFlag](https://docs.python.org/3/library/enum.html#enum.IntFlag) membership. [IntFlag](https://docs.python.org/3/library/enum.html#enum.IntFlag) members are also subclasses of [int](https://docs.python.org/3/library/functions.html#int). ([Notes](https://docs.python.org/3/library/enum.html#notes))
> 

I do not want to say that this should be part of this PR, but just wanted to make you aware of the Python enum module extend.

author_association: CONTRIBUTOR
user: login: rwgk
created_at: 2025-03-23T07:12:48Z
body: @henryiii @timohl @virtuald Could you please take another look? Hopefully this is final now.

* `py::enum_kind` is gone. The API is simply `py::native_enum<altitude>(m, "altitude", "enum.Enum")` now, with `"enum.Enum"` as the default. See the updated documentation. I believe with this we maximize flexibility and ease of use.

* The [related code changes](https://github.com/pybind/pybind11/pull/5555/commits/8efb9bf81a8d53e5c9b7ec9b9bd690bbdad91d54) — relative to what you reviewed before — are slightly messy at first sight but really just boilerplate changes.

* The only noteworthy new block of code is the `import_or_getattr()` function in include/pybind11/detail/native_enum_data.h. Most of the code is for comprehensive error reporting.
author_association: COLLABORATOR
user: login: timohl
created_at: 2025-03-23T11:22:33Z
body: Does it already support all standard Python enums, e.g., `StrEnum`, by just changing the string and if not, is there a clear way of extending?
I see the three requirements for the passed type name in the docs and after a quick look through the tests, I see `enum.IntEnum` and `enum.IntFlag`.
Could we maybe add small tests for all supported standard Python enums ([see Python docs](https://docs.python.org/3/library/enum.html#module-contents)).

I will take a deeper look later today.
author_association: CONTRIBUTOR
user: login: rwgk
created_at: 2025-03-23T16:20:06Z
body: > Does it already support all standard Python enums, e.g., `StrEnum`, by just changing the string and if not, is there a clear way of extending?

I don't think I shared this before (the convo is from last weekend):

* https://chatgpt.com/share/67d5e965-ccb0-8008-95b7-0df2502309b3

There are only two prompts. In the first I asked ChatGPT to explain `StrEnum`, in the second I asked "Is there are meaningful mapping to C++ enums?"

My interpretation of the answer: Not really.

But, with the new `native_type_name` API I believe you could implement a Python type that, roughly speaking, has the same constructor as `enum.Enum`, and the same behavior as `enum.StrEnum`.

Is that a wise thing to spend effort on? — I doubt it. — Certainly it's out of the realm I want to spend my effort on.

> I see the three requirements for the passed type name in the docs and after a quick look through the tests, I see `enum.IntEnum` and `enum.IntFlag`. Could we maybe add small tests for all supported standard Python enums ([see Python docs](https://docs.python.org/3/library/enum.html#module-contents)).

C++ enums are different from Python enums, roughly speaking again, a subset. Trying to stretch that subset to fully map to the larger set is a mistake IMO, especially because you can now implement Python types to handle specific use cases.

I added the simple `enum.IntFlag` test to convince myself that no production code changes are needed. I decided to not add e.g. `enum.Flag` because it seems too obvious to me that we'd just be turning test_native_enum.py into a pseudo unit test for `enum`, which isn't a good direction again, IMO.
author_association: COLLABORATOR
user: login: rwgk
created_at: 2025-03-24T04:21:12Z
body: > Just for the documentation, in addition to the three requirements, it would be nice to explicitly list what Python stdlib Enums are working and which are not. Then, newcomers would not have to study the Python docs or go by trial and error to find if `StrEnum` or `IntFlag` works. Currently, `Enum` and `IntEnum` are mentioned in the docs.

Done: commit ae07bd261be88fe83975e75fb7cdc36e215eb409

Thanks for the review!

(I did NOT systematically try the other enum module types, but I know for sure that `StrEnum` is not compatible with the requirements, and I believe all the other types are obviously not compatible, or are obviously not a fit at all.)
author_association: COLLABORATOR
user: login: henryiii
created_at: 2025-03-25T03:26:06Z
body: When we write the upgrade guide, we'll want to include `py::enum_` -> `py::native_enum` in it.

Nice PR number. :)
author_association: COLLABORATOR
user: login: rwgk
created_at: 2025-03-25T03:31:53Z
body: Thanks for the reviews! The API changes made this much better.
author_association: COLLABORATOR