diff --git a/python/pybind11_v3/pybind11/functional.h b/python/pybind11_v3/pybind11/functional.h
index 8f59f5fe5..5a554abad 100644
--- a/python/pybind11_v3/pybind11/functional.h
+++ b/python/pybind11_v3/pybind11/functional.h
@@ -29,12 +29,18 @@ struct func_handle {
     }
     func_handle(const func_handle &f_) { operator=(f_); }
     func_handle &operator=(const func_handle &f_) {
-        gil_scoped_acquire acq;
+        // Called on the way into a Regina function that expects
+        // a std::function argument.  In such situations the
+        // interpreter should already be holding the GIL.
+        // gil_scoped_acquire acq;
         f = f_.f;
         return *this;
     }
     ~func_handle() {
-        gil_scoped_acquire acq;
+        // Called on the way into and also out of a Regina function
+        // that expects a std::function argument.  In such situations
+        // the interpreter should already be holding the GIL.
+        // gil_scoped_acquire acq;
         function kill_f(std::move(f));
     }
 };
@@ -49,7 +55,10 @@ template <typename Return, typename... Args>
 struct func_wrapper : func_wrapper_base {
     using func_wrapper_base::func_wrapper_base;
     Return operator()(Args... args) const { // NOLINT(performance-unnecessary-value-param)
-        gil_scoped_acquire acq;
+        // Called when a std::function is executed as a callback within
+        // one of Regina's own functions.  In such situations Regina's
+        // Python bindings are responsible for ensuring the GIL is held.
+        // gil_scoped_acquire acq;
         // casts the returned object as a rvalue to the return type
         return hfunc.f(std::forward<Args>(args)...).template cast<Return>();
     }
diff --git a/python/pybind11_v3/pybind11/gil.h b/python/pybind11_v3/pybind11/gil.h
index 4222a035f..fef69b641 100644
--- a/python/pybind11_v3/pybind11/gil.h
+++ b/python/pybind11_v3/pybind11/gil.h
@@ -197,3 +197,30 @@ private:
 PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
 
 #endif // !PYBIND11_SIMPLE_GIL_MANAGEMENT
+
+PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
+
+/**
+ * A less lightweight version of gil_scoped_acquire that actually checks
+ * whether we are already holding the GIL before trying to acquire it.
+ *
+ * This is needed (for example) in PacketListener callbacks in the GUI, since
+ * we do not know whether listener methods are reacting to some action in the
+ * C++ GUI (where the GIL will not be held) or in a Python console (where the
+ * GIL will be held).
+ *
+ * - Ben Burton, 25/06/2025.
+ */
+class safe_gil_scoped_acquire {
+    std::optional<gil_scoped_acquire> gil;
+public:
+    safe_gil_scoped_acquire() {
+        if (! PyGILState_Check())
+            gil.emplace();
+    }
+    safe_gil_scoped_acquire(const safe_gil_scoped_acquire&) = delete;
+    safe_gil_scoped_acquire& operator = (const safe_gil_scoped_acquire&) =
+        delete;
+};
+
+PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
diff --git a/python/pybind11_v3/pybind11/pybind11.h b/python/pybind11_v3/pybind11/pybind11.h
index 06be7f1d4..d20de7ecc 100644
--- a/python/pybind11_v3/pybind11/pybind11.h
+++ b/python/pybind11_v3/pybind11/pybind11.h
@@ -33,6 +33,10 @@
 #include <utility>
 #include <vector>
 
+// This allows us to verify that we are #including our own patched pybind11,
+// not some other pybind11 that has been installed system-wide.
+#define REGINA_PYBIND11 1
+
 // See PR #5448. This warning suppression is needed for the PYBIND11_OVERRIDE macro family.
 // NOTE that this is NOT embedded in a push/pop pair because that is very difficult to achieve.
 #if defined(__clang_major__) && __clang_major__ < 14
@@ -43,6 +47,10 @@ PYBIND11_WARNING_DISABLE_CLANG("-Wgnu-zero-variadic-macro-arguments")
 #    include <cxxabi.h>
 #endif
 
+namespace regina {
+    const char* pythonTypename(const std::type_info*);
+}
+
 PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
 
 /* https://stackoverflow.com/questions/46798456/handling-gccs-noexcept-type-warning
@@ -101,6 +109,16 @@ inline std::string replace_newlines_and_squash(const char *text) {
     return result.substr(str_begin, str_range);
 }
 
+/* Regina-specific tweak to generate_function_signature */
+inline std::string fix_regina(const std::string& modulename, const std::string& qualname) {
+    if (modulename == "regina.engine")
+        return "regina." + qualname;
+    else if (modulename == "regina.engine.internal")
+        return "regina.internal." + qualname;
+    else
+        return modulename + "." + qualname;
+}
+
 /* Generate a proper function signature */
 inline std::string generate_function_signature(const char *type_caster_name_field,
                                                detail::function_record *func_rec,
@@ -157,18 +175,43 @@ inline std::string generate_function_signature(const char *type_caster_name_fiel
             if (!t) {
                 pybind11_fail("Internal error while parsing type signature (1)");
             }
-            if (auto *tinfo = detail::get_type_info(*t)) {
+            if (const char* name = regina::pythonTypename(t)) {
+                signature += name;
+            } else if (auto *tinfo = detail::get_type_info(*t)) {
                 handle th((PyObject *) tinfo->type);
-                signature += th.attr("__module__").cast<std::string>() + "."
-                             + th.attr("__qualname__").cast<std::string>();
+                std::string qualname;
+                try {
+                    qualname = th.attr("__qualname__").cast<std::string>();
+                } catch (...) {
+                    // In Python 3.12 we get an error in our stripped-down
+                    // interpreter since PythonOutputStream is missing both
+                    // __qualname__ and __name__.  Do not make this fatal.
+                    qualname = "<unknown>";
+                }
+                const auto m = th.attr("__module__").cast<std::string>();
+                signature += fix_regina(m, qualname);
             } else if (auto th = detail::global_internals_native_enum_type_map_get_item(*t)) {
-                signature += th.attr("__module__").cast<std::string>() + "."
-                             + th.attr("__qualname__").cast<std::string>();
+                std::string qualname;
+                try {
+                    qualname = th.attr("__qualname__").cast<std::string>();
+                } catch (...) {
+                    // See above for the explanation here.
+                    qualname = "<unknown>";
+                }
+                const auto m = th.attr("__module__").cast<std::string>();
+                signature += fix_regina(m, qualname);
             } else if (func_rec->is_new_style_constructor && arg_index == 0) {
                 // A new-style `__init__` takes `self` as `value_and_holder`.
                 // Rewrite it to the proper class type.
-                signature += func_rec->scope.attr("__module__").cast<std::string>() + "."
-                             + func_rec->scope.attr("__qualname__").cast<std::string>();
+                std::string qualname;
+                try {
+                    qualname = func_rec->scope.attr("__qualname__").cast<std::string>();
+                } catch (...) {
+                    // See above for the explanation here.
+                    qualname = "<unknown>";
+                }
+                const auto m = func_rec->scope.attr("__module__").cast<std::string>();
+                signature += fix_regina(m, qualname);
             } else {
                 signature += detail::quote_cpp_type_name(detail::clean_type_id(t->name()));
             }
@@ -1110,6 +1153,16 @@ protected:
         } catch (abi::__forced_unwind &) {
             throw;
 #endif
+        } catch (const pybind11::stop_iteration& stop) {
+            /* We prioritise catching stop_iteration before any of the other
+               exception logic below, since stop_iteration is arguably the one
+               setting where exceptions are normal as opposed to "exceptional".
+               The default exception logic involves many try/catch/rethrow
+               blocks, and in settings where try/catch is slow (e.g., running
+               within SageMath on macOS), this can add to a noticeable
+               performance penalty when using iterators. */
+            PyErr_SetString(PyExc_StopIteration, stop.what());
+            return nullptr;
         } catch (...) {
             try_translate_exceptions();
             return nullptr;
@@ -3213,10 +3266,10 @@ template <typename type>
 class exception : public object {
 public:
     exception() = default;
-    exception(handle scope, const char *name, handle base = PyExc_Exception) {
+    exception(handle scope, const char *name, const char* doc, handle base) {
         std::string full_name
             = scope.attr("__name__").cast<std::string>() + std::string(".") + name;
-        m_ptr = PyErr_NewException(const_cast<char *>(full_name.c_str()), base.ptr(), nullptr);
+        m_ptr = PyErr_NewExceptionWithDoc(const_cast<char *>(full_name.c_str()), doc, base.ptr(), nullptr);
         if (hasattr(scope, "__dict__") && scope.attr("__dict__").contains(name)) {
             pybind11_fail("Error during initialization: multiple incompatible "
                           "definitions with name \""
@@ -3521,7 +3521,7 @@ function get_override(const T *this_ptr, const char *name) {
 
 #define PYBIND11_OVERRIDE_IMPL(ret_type, cname, name, ...)                                        \
     do {                                                                                          \
-        pybind11::gil_scoped_acquire gil;                                                         \
+        pybind11::safe_gil_scoped_acquire gil;                                                    \
         pybind11::function override                                                               \
             = pybind11::get_override(static_cast<const cname *>(this), name);                     \
         if (override) {                                                                           \
diff --git a/python/pybind11_v3/pybind11/subinterpreter.h b/python/pybind11_v3/pybind11/subinterpreter.h
index 9f2f704c5..cb666cf28 100644
--- a/python/pybind11_v3/pybind11/subinterpreter.h
+++ b/python/pybind11_v3/pybind11/subinterpreter.h
@@ -21,7 +21,7 @@
 
 PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
 PYBIND11_NAMESPACE_BEGIN(detail)
-PyInterpreterState *get_interpreter_state_unchecked() {
+inline PyInterpreterState *get_interpreter_state_unchecked() {
     auto cur_tstate = get_thread_state_unchecked();
     if (cur_tstate)
         return cur_tstate->interp;
