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 `_
+(see also `#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(m, "Pet");
+
+ pet.def(py::init())
+ .def_readwrite("name", &Pet::name)
+ .def_readwrite("type", &Pet::type)
+ .def_readwrite("attr", &Pet::attr);
+
+ py::enum_(pet, "Kind")
+ .value("Dog", Pet::Kind::Dog)
+ .value("Cat", Pet::Kind::Cat)
+ .export_values();
+
+ py::class_(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", 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 `_ 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 // Not already included with pybind11.h
+
py::class_ pet(m, "Pet");
pet.def(py::init())
@@ -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")
+ py::native_enum(pet, "Kind")
.value("Dog", Pet::Kind::Dog)
.value("Cat", Pet::Kind::Cat)
- .export_values();
+ .export_values()
+ .finalize();
py::class_(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 `_.
+This is the recommended way to bind C++ enums.
+The older ``py::enum_`` is not PEP 435 compatible
+(see `issue #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 `.
-.. 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", "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
+ `_::
- >>> 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 `_
+ 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
+`_ 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 `. In such cases, a
+ template specialization like this may be required:
.. code-block:: cpp
- py::enum_(pet, "Kind", py::arithmetic())
- ...
-
- By default, these are omitted to conserve space.
+ #if defined(PYBIND11_HAS_NATIVE_ENUM)
+ namespace pybind11::detail {
+ template
+ struct type_caster_enum_type_enabled<
+ FancyEnum,
+ std::enable_if_t::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 `_ 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 &&caster) {
return std::move(caster).operator result_t();
}
+template
+class type_caster_enum_type {
+private:
+ using Underlying = typename std::underlying_type::type;
+
+public:
+ static constexpr auto name = const_name();
+
+ template
+ 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(src)).release();
+ }
+ return type_caster_base::cast(
+ std::forward(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_caster;
+ if (!underlying_caster.load(src.attr("value"), convert)) {
+ pybind11_fail("native_enum internal consistency failure.");
+ }
+ value = static_cast(static_cast(underlying_caster));
+ return true;
+ }
+ if (!pybind11_enum_) {
+ pybind11_enum_.reset(new type_caster_base());
+ }
+ return pybind11_enum_->load(src, convert);
+ }
+
+ template
+ using cast_op_type = detail::cast_op_type;
+
+ // 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> pybind11_enum_;
+ EnumType value;
+};
+
+template
+struct type_caster_enum_type_enabled : std::true_type {};
+
+template
+struct type_uses_type_caster_enum_type {
+ static constexpr bool value
+ = std::is_enum::value && type_caster_enum_type_enabled::value;
+};
+
+template
+class type_caster::value>>
+ : public type_caster_enum_type {};
+
+template ::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 ::value, int> = 0>
+bool isinstance_native_enum_impl(handle, const std::type_info &) {
+ return false;
+}
+
+template
+bool isinstance_native_enum(handle obj, const std::type_info &tp) {
+ return isinstance_native_enum_impl>(obj, tp);
+}
+
template
class type_caster> {
private:
@@ -1470,8 +1569,17 @@ template
T cast(const handle &handle) {
using namespace detail;
- static_assert(!cast_is_temporary_value_reference::value,
+ constexpr bool is_enum_cast = type_uses_type_caster_enum_type>::value;
+ static_assert(!cast_is_temporary_value_reference::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::value) {
+ if (detail::global_internals_native_enum_type_map_contains(
+ std::type_index(typeid(intrinsic_t)))) {
+ pybind11_fail("Unable to cast native enum type to reference");
+ }
+ }
+#endif
return cast_op(load_type(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() + "." + 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 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
+#include
+#include
+#include
+
+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