From 3bc545180441deec97f7bbe95fbce7d0db184c01 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 24 Jul 2026 23:46:49 -0400 Subject: [PATCH 01/10] feat!: drop support for Python 3.8 The minimum supported version is now Python 3.9. pybind11 v3.0 was the last release that supports Python 3.8. The deprecation note said that support goes away in 3.1. Remove the code paths that this makes dead: - the `_PyObject_Vectorcall` fallback in `cast.h` - the `frame->f_code` and `frame->f_back` fallbacks in `pytypes.h` - the `PyFrame_FastToLocals` path in `get_type_override` - the conditional `Py_VISIT(Py_TYPE(self))` in `tp_traverse` - `PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX`, the pre-PyConfig interpreter init, and the `widen_chars` helpers in `embed.h` Assisted-by: ClaudeCode:claude-opus-5 --- .github/workflows/ci.yml | 18 +++--- .github/workflows/pip.yml | 12 ++-- .github/workflows/reusable-standard.yml | 2 +- AGENTS.md | 2 +- README.rst | 4 +- docs/advanced/classes.rst | 4 +- docs/advanced/deprecated.rst | 2 - docs/compiling.rst | 4 +- include/pybind11/cast.h | 9 +-- include/pybind11/detail/class.h | 4 +- include/pybind11/detail/common.h | 4 +- .../detail/function_record_pyobject.h | 7 +-- include/pybind11/embed.h | 57 ------------------- include/pybind11/pybind11.h | 25 ++------ include/pybind11/pytypes.h | 10 ---- pybind11/__init__.py | 4 +- pybind11/setup_helpers.py | 19 +++---- pyproject.toml | 7 +-- tests/extra_python_package/test_files.py | 2 +- tests/requirements.txt | 3 +- tests/test_builtin_casters.py | 6 +- tests/test_custom_type_setup.cpp | 4 +- tests/test_docs_advanced_cast_custom.py | 3 +- tests/test_exceptions.py | 10 +--- tests/test_multiple_interpreters.py | 7 ++- tests/test_with_catch/test_interpreter.cpp | 6 +- tools/FindPythonLibsNew.cmake | 2 +- tools/pybind11NewTools.cmake | 6 +- tools/pybind11Tools.cmake | 6 +- 29 files changed, 68 insertions(+), 181 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ad1178395..492239277b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: matrix: include: - runs-on: ubuntu-22.04 - python-version: '3.8' + python-version: '3.9' cmake-args: -DPYBIND11_FINDPYTHON=OFF -DPYBIND11_NUMPY_1_ONLY=ON - runs-on: ubuntu-latest python-version: '3.13' @@ -60,7 +60,7 @@ jobs: python-version: '3.14' cmake-args: -DCMAKE_CXX_STANDARD=14 - runs-on: windows-2022 - python-version: '3.8' + python-version: '3.9' cmake-args: -DPYBIND11_FINDPYTHON=OFF @@ -78,7 +78,7 @@ jobs: matrix: include: - runs-on: ubuntu-latest - python-version: '3.8' + python-version: '3.9' cmake-args: -DPYBIND11_FINDPYTHON=ON -DCMAKE_CXX_STANDARD=17 - runs-on: ubuntu-latest python-version: '3.10' @@ -95,7 +95,7 @@ jobs: # No SciPy for macOS ARM - runs-on: macos-15-intel - python-version: '3.8' + python-version: '3.9' cmake-args: -DCMAKE_CXX_STANDARD=14 - runs-on: macos-15-intel python-version: '3.13' @@ -118,7 +118,7 @@ jobs: python-version: '3.9' cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON - runs-on: windows-2022 - python-version: '3.8' + python-version: '3.9' cmake-args: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DPYBIND11_NUMPY_1_ONLY=ON # This needs a python built with MTd # - runs-on: windows-2022 @@ -187,7 +187,7 @@ jobs: - name: Prepare env run: uv pip install --python=python --system -r tests/requirements.txt - # TODO Resolve Windows Ninja shared object issue on Python 3.8+ + # TODO Resolve Windows Ninja shared object issue on Python 3.9+ - name: Use Ninja except on Windows if: runner.os != 'Windows' run: echo "CMAKE_GENERATOR=Ninja" >> "$GITHUB_ENV" @@ -629,9 +629,9 @@ jobs: - name: Latest actions/checkout uses: actions/checkout@v7 - - name: Add Python 3.8 + - name: Add Python 3.9 if: matrix.container == 'almalinux:8' - run: dnf update -y && dnf install -y python38-devel gcc-c++ make git + run: dnf update -y && dnf install -y python39-devel gcc-c++ make git - name: Add Python 3 (default) if: matrix.container != 'almalinux:8' @@ -767,7 +767,7 @@ jobs: fail-fast: false matrix: include: - - python: '3.8' + - python: '3.9' args: -DCMAKE_CXX_STANDARD=17 - python: '3.13' diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index 8d47802526..91279de771 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -19,16 +19,16 @@ jobs: # This builds the sdists and wheels and makes sure the files are exactly as # expected. test-packaging: - name: 🐍 3.8 β€’ πŸ“¦ tests β€’ windows-latest + name: 🐍 3.9 β€’ πŸ“¦ tests β€’ windows-latest runs-on: windows-latest steps: - uses: actions/checkout@v7 - - name: Setup 🐍 3.8 + - name: Setup 🐍 3.9 uses: actions/setup-python@v6 with: - python-version: 3.8 + python-version: 3.9 - name: Install uv uses: astral-sh/setup-uv@v8.2.0 @@ -43,16 +43,16 @@ jobs: # This runs the packaging tests and also builds and saves the packages as # artifacts. packaging: - name: 🐍 3.8 β€’ πŸ“¦ & πŸ“¦ tests β€’ ubuntu-latest + name: 🐍 3.9 β€’ πŸ“¦ & πŸ“¦ tests β€’ ubuntu-latest runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - name: Setup 🐍 3.8 + - name: Setup 🐍 3.9 uses: actions/setup-python@v6 with: - python-version: 3.8 + python-version: 3.9 - name: Install uv uses: astral-sh/setup-uv@v8.2.0 diff --git a/.github/workflows/reusable-standard.yml b/.github/workflows/reusable-standard.yml index 6081f801eb..1231bcf929 100644 --- a/.github/workflows/reusable-standard.yml +++ b/.github/workflows/reusable-standard.yml @@ -63,7 +63,7 @@ jobs: if: runner.os == 'Linux' run: uv pip install --python=python --system pytest-github-actions-annotate-failures - # TODO Resolve Windows Ninja shared object issue on Python 3.8+ + # TODO Resolve Windows Ninja shared object issue on Python 3.9+ - name: Use Ninja except on Windows if: runner.os != 'Windows' run: echo "CMAKE_GENERATOR=Ninja" >> "$GITHUB_ENV" diff --git a/AGENTS.md b/AGENTS.md index d3e09857ff..4108925520 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,5 +95,5 @@ Packaging produces two distributions: the normal `pybind11` (headers live inside - Any new functionality needs a test (add to or create the paired `.cpp`/`.py` in `tests/`, and register the test in `tests/CMakeLists.txt`). - Bug fixes should be paired with new tests that fail without the fix. - Try to add to an existing test file if possible (more files slow down tests) -- Default C++ standard follows the consumer's toolchain; CI exercises a wide matrix (CPython 3.8+, PyPy, GraalPy; multiple compilers and C++ standards). Keep changes portable across that matrix. +- Default C++ standard follows the consumer's toolchain; CI exercises a wide matrix (CPython 3.9+, PyPy, GraalPy; multiple compilers and C++ standards). Keep changes portable across that matrix. - PR descriptions follow a template, the key part is under "Suggested changelog entry", which is how we generate our changelog (with `nox -s make_changelog`) diff --git a/README.rst b/README.rst index 26d618f93a..77f88059b4 100644 --- a/README.rst +++ b/README.rst @@ -36,7 +36,7 @@ dependency. Think of this library as a tiny self-contained version of Boost.Python with everything stripped away that isn't relevant for binding generation. Without comments, the core header files only require ~4K -lines of code and depend on Python (CPython 3.8+, PyPy, or GraalPy) and the C++ +lines of code and depend on Python (CPython 3.9+, PyPy, or GraalPy) and the C++ standard library. This compact implementation was possible thanks to some C++11 language features (specifically: tuples, lambda functions and variadic templates). Since its creation, this library has grown beyond Boost.Python in @@ -81,7 +81,7 @@ Goodies In addition to the core functionality, pybind11 provides some extra goodies: -- CPython 3.8+, PyPy3 7.3.17+, and GraalPy 24.1+ are supported with an +- CPython 3.9+, PyPy3 7.3.17+, and GraalPy 24.1+ are supported with an implementation-agnostic interface (see older versions for older CPython and PyPy versions). diff --git a/docs/advanced/classes.rst b/docs/advanced/classes.rst index bfbaea60ab..2954411d7b 100644 --- a/docs/advanced/classes.rst +++ b/docs/advanced/classes.rst @@ -1400,10 +1400,8 @@ You can do that using ``py::custom_type_setup``: auto *type = &heap_type->ht_type; type->tp_flags |= Py_TPFLAGS_HAVE_GC; type->tp_traverse = [](PyObject *self_base, visitproc visit, void *arg) { - // https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse - #if PY_VERSION_HEX >= 0x03090000 + // https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse Py_VISIT(Py_TYPE(self_base)); - #endif if (py::detail::is_holder_constructed(self_base)) { auto &self = py::cast(py::handle(self_base)); for (auto &item : self.list) { diff --git a/docs/advanced/deprecated.rst b/docs/advanced/deprecated.rst index dc07a77b94..c9f4a2e666 100644 --- a/docs/advanced/deprecated.rst +++ b/docs/advanced/deprecated.rst @@ -3,8 +3,6 @@ Deprecated ########## -Support for Python 3.8 is deprecated and will be removed in 3.1. - Support for C++11 is deprecated and will be removed in a future version. Please use at least C++14. diff --git a/docs/compiling.rst b/docs/compiling.rst index b693bd5878..fb21e16edf 100644 --- a/docs/compiling.rst +++ b/docs/compiling.rst @@ -429,7 +429,7 @@ with ``PYTHON_EXECUTABLE``. For example: .. code-block:: bash - cmake -DPYBIND11_PYTHON_VERSION=3.8 .. + cmake -DPYBIND11_PYTHON_VERSION=3.9 .. # Another method: cmake -DPYTHON_EXECUTABLE=/path/to/python .. @@ -495,7 +495,7 @@ FindPython, pybind11 will detect this and use the existing targets instead: cmake_minimum_required(VERSION 3.15...4.2) project(example LANGUAGES CXX) - find_package(Python 3.8 COMPONENTS Interpreter Development REQUIRED) + find_package(Python 3.9 COMPONENTS Interpreter Development REQUIRED) find_package(pybind11 CONFIG REQUIRED) # or add_subdirectory(pybind11) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 62ca45a09a..93dd1c458f 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -2251,13 +2251,8 @@ class unpacking_collector { if (m_names) { nargs -= m_names.size(); } - PyObject *result = -#if PY_VERSION_HEX >= 0x03090000 - PyObject_Vectorcall( -#else - _PyObject_Vectorcall( -#endif - ptr, m_args.data() + 1, nargs | PY_VECTORCALL_ARGUMENTS_OFFSET, m_names.ptr()); + PyObject *result = PyObject_Vectorcall( + ptr, m_args.data() + 1, nargs | PY_VECTORCALL_ARGUMENTS_OFFSET, m_names.ptr()); if (!result) { throw error_already_set(); } diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index 8b9d0b8e9d..06a761de54 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -586,10 +586,8 @@ extern "C" inline int pybind11_traverse(PyObject *self, visitproc visit, void *a PyObject *&dict = *_PyObject_GetDictPtr(self); Py_VISIT(dict); #endif -// https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse -#if PY_VERSION_HEX >= 0x03090000 + // https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse Py_VISIT(Py_TYPE(self)); -#endif return 0; } diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index e22763c99e..00f2850512 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -10,8 +10,8 @@ #pragma once #include -#if PY_VERSION_HEX < 0x03080000 -# error "PYTHON < 3.8 IS UNSUPPORTED. pybind11 v2.13 was the last to support Python 3.7." +#if PY_VERSION_HEX < 0x03090000 +# error "PYTHON < 3.9 IS UNSUPPORTED. pybind11 v3.0 was the last to support Python 3.8." #endif // Similar to Python's convention: https://docs.python.org/3/c-api/apiabiversion.html diff --git a/include/pybind11/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h index 94d27ad17b..42b0e96be7 100644 --- a/include/pybind11/detail/function_record_pyobject.h +++ b/include/pybind11/detail/function_record_pyobject.h @@ -48,8 +48,8 @@ static PyMethodDef tp_methods_impl[] // Python 3.12+ emits a DeprecationWarning for heap types whose tp_name does // not contain a dot ('.') and that lack a __module__ attribute. For pybind11's // internal function_record type, we do not have an actual module object to -// attach, so we cannot use PyType_FromModuleAndSpec (introduced in Python 3.9) -// to set __module__ automatically. +// attach, so we cannot use PyType_FromModuleAndSpec to set __module__ +// automatically. // // As a workaround, we define a "qualified" type name that includes a dummy // module name (PYBIND11_DUMMY_MODULE_NAME). This is non‑idiomatic but avoids @@ -59,8 +59,7 @@ static PyMethodDef tp_methods_impl[] // // even though no real pybind11_builtins module exists. If pybind11 gains an // actual module object in the future, this code should switch to -// PyType_FromModuleAndSpec for Python 3.9+ and drop the dummy module -// workaround. +// PyType_FromModuleAndSpec and drop the dummy module workaround. // // Note that this name is versioned. #define PYBIND11_DETAIL_FUNCTION_RECORD_TP_PLAINNAME \ diff --git a/include/pybind11/embed.h b/include/pybind11/embed.h index c05887c33d..94be0d102d 100644 --- a/include/pybind11/embed.h +++ b/include/pybind11/embed.h @@ -12,9 +12,6 @@ #include "pybind11.h" #include "eval.h" -#include -#include - #if defined(PYPY_VERSION) # error Embedding the interpreter is not supported with PyPy #endif @@ -82,60 +79,14 @@ struct embedded_module { } }; -struct wide_char_arg_deleter { - void operator()(wchar_t *ptr) const { - // API docs: https://docs.python.org/3/c-api/sys.html#c.Py_DecodeLocale - PyMem_RawFree(ptr); - } -}; - -inline wchar_t *widen_chars(const char *safe_arg) { - wchar_t *widened_arg = Py_DecodeLocale(safe_arg, nullptr); - return widened_arg; -} - inline void precheck_interpreter() { if (Py_IsInitialized() != 0) { pybind11_fail("The interpreter is already running"); } } -#if !defined(PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX) -# define PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX (0x03080000) -#endif - -#if PY_VERSION_HEX < PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX -inline void initialize_interpreter_pre_pyconfig(bool init_signal_handlers, - int argc, - const char *const *argv, - bool add_program_dir_to_path) { - detail::precheck_interpreter(); - Py_InitializeEx(init_signal_handlers ? 1 : 0); - - auto argv_size = static_cast(argc); - // SetArgv* on python 3 takes wchar_t, so we have to convert. - std::unique_ptr widened_argv(new wchar_t *[argv_size]); - std::vector> widened_argv_entries; - widened_argv_entries.reserve(argv_size); - for (size_t ii = 0; ii < argv_size; ++ii) { - widened_argv_entries.emplace_back(detail::widen_chars(argv[ii])); - if (!widened_argv_entries.back()) { - // A null here indicates a character-encoding failure or the python - // interpreter out of memory. Give up. - return; - } - widened_argv[ii] = widened_argv_entries.back().get(); - } - - auto *pysys_argv = widened_argv.get(); - - PySys_SetArgvEx(argc, pysys_argv, static_cast(add_program_dir_to_path)); -} -#endif - PYBIND11_NAMESPACE_END(detail) -#if PY_VERSION_HEX >= PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX inline void initialize_interpreter(PyConfig *config, int argc = 0, const char *const *argv = nullptr, @@ -163,7 +114,6 @@ inline void initialize_interpreter(PyConfig *config, } PyConfig_Clear(config); } -#endif /** \rst Initialize the Python interpreter. No other pybind11 or CPython API functions can be @@ -188,10 +138,6 @@ inline void initialize_interpreter(bool init_signal_handlers = true, int argc = 0, const char *const *argv = nullptr, bool add_program_dir_to_path = true) { -#if PY_VERSION_HEX < PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX - detail::initialize_interpreter_pre_pyconfig( - init_signal_handlers, argc, argv, add_program_dir_to_path); -#else PyConfig config; PyConfig_InitPythonConfig(&config); // See PR #4473 for background @@ -199,7 +145,6 @@ inline void initialize_interpreter(bool init_signal_handlers = true, config.install_signal_handlers = init_signal_handlers ? 1 : 0; initialize_interpreter(&config, argc, argv, add_program_dir_to_path); -#endif // There is exactly one interpreter alive currently. detail::has_seen_non_main_interpreter() = false; @@ -293,14 +238,12 @@ class scoped_interpreter { initialize_interpreter(init_signal_handlers, argc, argv, add_program_dir_to_path); } -#if PY_VERSION_HEX >= PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX explicit scoped_interpreter(PyConfig *config, int argc = 0, const char *const *argv = nullptr, bool add_program_dir_to_path = true) { initialize_interpreter(config, argc, argv, add_program_dir_to_path); } -#endif scoped_interpreter(const scoped_interpreter &) = delete; scoped_interpreter(scoped_interpreter &&other) noexcept { other.is_valid = false; } diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index ccd7baee02..159ecd28e7 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -3837,24 +3837,23 @@ get_type_override(const void *this_ptr, const type_info *this_type, const char * /* Don't call dispatch code if invoked from overridden function. Unfortunately this doesn't work on PyPy and GraalPy. */ #if !defined(PYPY_VERSION) && !defined(GRAALVM_PYTHON) -# if PY_VERSION_HEX >= 0x03090000 PyFrameObject *frame = PyThreadState_GetFrame(PyThreadState_Get()); if (frame != nullptr) { PyCodeObject *f_code = PyFrame_GetCode(frame); // f_code is guaranteed to not be NULL if (std::string(str(f_code->co_name)) == name && f_code->co_argcount > 0) { -# if PY_VERSION_HEX >= 0x030d0000 +# if PY_VERSION_HEX >= 0x030d0000 PyObject *locals = PyEval_GetFrameLocals(); -# else +# else PyObject *locals = PyEval_GetLocals(); Py_XINCREF(locals); -# endif +# endif if (locals != nullptr) { -# if PY_VERSION_HEX >= 0x030b0000 +# if PY_VERSION_HEX >= 0x030b0000 PyObject *co_varnames = PyCode_GetVarnames(f_code); -# else +# else PyObject *co_varnames = PyObject_GetAttrString((PyObject *) f_code, "co_varnames"); -# endif +# endif PyObject *self_arg = PyTuple_GET_ITEM(co_varnames, 0); Py_DECREF(co_varnames); PyObject *self_caller = dict_getitem(locals, self_arg); @@ -3869,18 +3868,6 @@ get_type_override(const void *this_ptr, const type_info *this_type, const char * Py_DECREF(f_code); Py_DECREF(frame); } -# else - PyFrameObject *frame = PyThreadState_Get()->frame; - if (frame != nullptr && (std::string) str(frame->f_code->co_name) == name - && frame->f_code->co_argcount > 0) { - PyFrame_FastToLocals(frame); - PyObject *self_caller - = dict_getitem(frame->f_locals, PyTuple_GET_ITEM(frame->f_code->co_varnames, 0)); - if (self_caller == self.ptr()) { - return function(); - } - } -# endif #else /* PyPy currently doesn't provide a detailed cpyext emulation of diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index ff4be6a500..13a6ebaa4d 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -670,12 +670,7 @@ struct error_fetch_and_normalize { Py_XINCREF(frame); result += "\n\nAt:\n"; while (frame) { -# if PY_VERSION_HEX >= 0x030900B1 PyCodeObject *f_code = PyFrame_GetCode(frame); -# else - PyCodeObject *f_code = frame->f_code; - Py_INCREF(f_code); -# endif int lineno = PyFrame_GetLineNumber(frame); result += " "; result += handle(f_code->co_filename).cast(); @@ -685,12 +680,7 @@ struct error_fetch_and_normalize { result += handle(f_code->co_name).cast(); result += '\n'; Py_DECREF(f_code); -# if PY_VERSION_HEX >= 0x030900B1 auto *b_frame = PyFrame_GetBack(frame); -# else - auto *b_frame = frame->f_back; - Py_XINCREF(b_frame); -# endif Py_DECREF(frame); frame = b_frame; } diff --git a/pybind11/__init__.py b/pybind11/__init__.py index e9d033c4b4..df5e8ee92a 100644 --- a/pybind11/__init__.py +++ b/pybind11/__init__.py @@ -2,8 +2,8 @@ import sys -if sys.version_info < (3, 8): # noqa: UP036 - msg = "pybind11 does not support Python < 3.8. v2.13 was the last release supporting Python 3.7." +if sys.version_info < (3, 9): # noqa: UP036 + msg = "pybind11 does not support Python < 3.9. v3.0 was the last release supporting Python 3.8." raise ImportError(msg) diff --git a/pybind11/setup_helpers.py b/pybind11/setup_helpers.py index 300688bf10..c4083756c6 100644 --- a/pybind11/setup_helpers.py +++ b/pybind11/setup_helpers.py @@ -48,16 +48,13 @@ import tempfile import threading import warnings +from collections.abc import Iterable, Iterator from functools import lru_cache from pathlib import Path from typing import ( Any, Callable, - Iterable, - Iterator, - List, Optional, - Tuple, TypeVar, Union, ) @@ -347,16 +344,16 @@ def no_recompile(obj: str, src: str) -> bool: # noqa: ARG001 CCompilerMethod = Callable[ [ distutils.ccompiler.CCompiler, - List[str], + list[str], Optional[str], - Optional[List[Union[Tuple[str], Tuple[str, Optional[str]]]]], - Optional[List[str]], + Optional[list[Union[tuple[str], tuple[str, Optional[str]]]]], + Optional[list[str]], bool, - Optional[List[str]], - Optional[List[str]], - Optional[List[str]], + Optional[list[str]], + Optional[list[str]], + Optional[list[str]], ], - List[str], + list[str], ] diff --git a/pyproject.toml b/pyproject.toml index c3395aa660..caf0a8b902 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ classifiers = [ "Topic :: Utilities", "Programming Language :: C++", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -34,7 +33,7 @@ keywords = [ "Python bindings", ] dynamic = ["version", "optional-dependencies"] -requires-python = ">=3.8" +requires-python = ">=3.9" [project.urls] Homepage = "https://github.com/pybind/pybind11" @@ -120,7 +119,7 @@ override-dependencies = ["pybind11-global"] [tool.mypy] files = ["pybind11"] -python_version = "3.8" +python_version = "3.9" strict = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] warn_unreachable = true @@ -131,7 +130,7 @@ ignore_missing_imports = true [tool.pylint] -master.py-version = "3.8" +master.py-version = "3.9" reports.output-format = "colorized" messages_control.disable = [ "design", diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index d96e9afc1f..e526899ec0 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -8,8 +8,8 @@ import sys import tarfile import zipfile +from collections.abc import Generator from pathlib import Path -from typing import Generator # These tests must be run explicitly diff --git a/tests/requirements.txt b/tests/requirements.txt index 1ba4026b46..e10fb40d29 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -3,11 +3,10 @@ build>=1 # No released numpy has cp315 wheels yet; drop the upper bound once it does. numpy>=2.4.0; platform_python_implementation=="CPython" and python_version>="3.14" and python_version<"3.15" -numpy~=1.23.0; python_version=="3.8" and platform_python_implementation=="PyPy" numpy~=1.25.0; python_version=="3.9" and platform_python_implementation=="PyPy" numpy~=2.2.0; python_version=="3.10" and platform_python_implementation=="PyPy" numpy~=2.2.0; platform_python_implementation=="GraalVM" and sys_platform=="linux" -numpy~=1.21.5; platform_python_implementation=="CPython" and python_version>="3.8" and python_version<"3.10" +numpy~=1.21.5; platform_python_implementation=="CPython" and python_version=="3.9" numpy~=1.22.2; platform_python_implementation=="CPython" and python_version=="3.10" numpy~=1.26.0; platform_python_implementation=="CPython" and python_version>="3.11" and python_version<"3.13" and platform_machine!="ARM64" numpy>=2.3.0; platform_python_implementation=="CPython" and python_version>="3.11" and platform_machine=="ARM64" diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index b232c087e0..09203502d5 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -302,7 +302,6 @@ def cant_convert(v): assert noconvert(7) == 7 cant_convert(3.14159) # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) - # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) if sys.version_info < (3, 10) and env.CPYTHON: with pytest.deprecated_call(): assert convert(Int()) == 42 @@ -312,8 +311,6 @@ def cant_convert(v): cant_convert(NotInt()) cant_convert(Float()) - # Before Python 3.8, `PyLong_AsLong` does not pick up on `obj.__index__`, - # but pybind11 "backports" this behavior. assert convert(Index()) == 42 assert isinstance(convert(Index()), int) assert noconvert(Index()) == 42 @@ -419,9 +416,8 @@ def require_implicit(v): # The implicit conversion from np.float32 is undesirable but currently accepted. # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) - # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) # https://github.com/pybind/pybind11/issues/3408 - if (3, 8) <= sys.version_info < (3, 10) and env.CPYTHON: + if sys.version_info < (3, 10) and env.CPYTHON: with pytest.deprecated_call(): assert convert(np.float32(3.14159)) == 3 else: diff --git a/tests/test_custom_type_setup.cpp b/tests/test_custom_type_setup.cpp index 15516b21b0..5235729e40 100644 --- a/tests/test_custom_type_setup.cpp +++ b/tests/test_custom_type_setup.cpp @@ -71,10 +71,8 @@ TEST_SUBMODULE(custom_type_setup, m) { auto *type = &heap_type->ht_type; type->tp_flags |= Py_TPFLAGS_HAVE_GC; type->tp_traverse = [](PyObject *self_base, visitproc visit, void *arg) { -// https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse -#if PY_VERSION_HEX >= 0x03090000 + // https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse Py_VISIT(Py_TYPE(self_base)); -#endif if (py::detail::is_holder_constructed(self_base)) { auto &self = py::cast(py::handle(self_base)); for (auto &item : self.list) { diff --git a/tests/test_docs_advanced_cast_custom.py b/tests/test_docs_advanced_cast_custom.py index 5374cbceb2..63d608c446 100644 --- a/tests/test_docs_advanced_cast_custom.py +++ b/tests/test_docs_advanced_cast_custom.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Sequence +from collections.abc import Sequence +from typing import TYPE_CHECKING if TYPE_CHECKING: from conftest import SanitizedString diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 1ee13ba3f3..1c70a9ea6e 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -92,17 +92,9 @@ def test_python_call_in_catch(): assert d["good"] is True -def ignore_pytest_unraisable_warning(f): - unraisable = "PytestUnraisableExceptionWarning" - if hasattr(pytest, unraisable): # Python >= 3.8 and pytest >= 6 - dec = pytest.mark.filterwarnings(f"ignore::pytest.{unraisable}") - return dec(f) - return f - - # TODO: find out why this fails on PyPy, https://foss.heptapod.net/pypy/pypy/-/issues/3583 @pytest.mark.xfail(env.PYPY, reason="Failure on PyPy 3.8 (7.3.7)", strict=False) -@ignore_pytest_unraisable_warning +@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") def test_python_alreadyset_in_destructor(monkeypatch, capsys): triggered = False diff --git a/tests/test_multiple_interpreters.py b/tests/test_multiple_interpreters.py index 65b2733191..6ca4fd8eee 100644 --- a/tests/test_multiple_interpreters.py +++ b/tests/test_multiple_interpreters.py @@ -157,9 +157,10 @@ def test_independent_subinterpreters_modern(): """ ).strip() - with contextlib.closing(interpreters.create()) as interp1, contextlib.closing( - interpreters.create() - ) as interp2: + with ( + contextlib.closing(interpreters.create()) as interp1, + contextlib.closing(interpreters.create()) as interp2, + ): with pytest.raises( interpreters.ExecutionFailed, match="does not support loading in subinterpreters", diff --git a/tests/test_with_catch/test_interpreter.cpp b/tests/test_with_catch/test_interpreter.cpp index 4103c0f5ff..709f66fc11 100644 --- a/tests/test_with_catch/test_interpreter.cpp +++ b/tests/test_with_catch/test_interpreter.cpp @@ -195,7 +195,6 @@ TEST_CASE("There can be only one interpreter") { py::initialize_interpreter(); } -#if PY_VERSION_HEX >= PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX TEST_CASE("Custom PyConfig") { py::finalize_interpreter(); PyConfig config; @@ -258,9 +257,8 @@ TEST_CASE("scoped_interpreter with PyConfig_InitPythonConfig and argv") { } py::initialize_interpreter(); } -#endif -TEST_CASE("Add program dir to path pre-PyConfig") { +TEST_CASE("Add program dir to path without PyConfig") { py::finalize_interpreter(); size_t path_size_add_program_dir_to_path_false = 0; { @@ -274,7 +272,6 @@ TEST_CASE("Add program dir to path pre-PyConfig") { py::initialize_interpreter(); } -#if PY_VERSION_HEX >= PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX TEST_CASE("Add program dir to path using PyConfig") { py::finalize_interpreter(); size_t path_size_add_program_dir_to_path_false = 0; @@ -292,7 +289,6 @@ TEST_CASE("Add program dir to path using PyConfig") { } py::initialize_interpreter(); } -#endif TEST_CASE("Restart the interpreter") { // Verify pre-restart state. diff --git a/tools/FindPythonLibsNew.cmake b/tools/FindPythonLibsNew.cmake index 7351bcaa65..d72e21332b 100644 --- a/tools/FindPythonLibsNew.cmake +++ b/tools/FindPythonLibsNew.cmake @@ -92,7 +92,7 @@ endif() # Use the Python interpreter to find the libs. if(NOT PythonLibsNew_FIND_VERSION) - set(PythonLibsNew_FIND_VERSION "3.8") + set(PythonLibsNew_FIND_VERSION "3.9") endif() if(NOT CMAKE_VERSION VERSION_LESS "3.27") diff --git a/tools/pybind11NewTools.cmake b/tools/pybind11NewTools.cmake index 568b0b0595..b0fe20768d 100644 --- a/tools/pybind11NewTools.cmake +++ b/tools/pybind11NewTools.cmake @@ -52,7 +52,7 @@ if(NOT Python_FOUND AND NOT Python3_FOUND) endif() find_package( - Python 3.8 REQUIRED COMPONENTS ${_pybind11_interp_component} ${_pybind11_dev_component} + Python 3.9 REQUIRED COMPONENTS ${_pybind11_interp_component} ${_pybind11_dev_component} ${_pybind11_quiet} ${_pybind11_global_keyword}) # If we are in submodule mode, export the Python targets to global targets. @@ -192,8 +192,8 @@ else() endif() endif() -# Python debug libraries expose slightly different objects before 3.8 -# https://docs.python.org/3.6/c-api/intro.html#debugging-builds +# A debug build of Python needs Py_DEBUG to select the matching ABI. +# https://docs.python.org/3/c-api/intro.html#debugging-builds # https://stackoverflow.com/questions/39161202/how-to-work-around-missing-pymodule-create2-in-amd64-win-python35-d-lib if(PYTHON_IS_DEBUG) set_property( diff --git a/tools/pybind11Tools.cmake b/tools/pybind11Tools.cmake index a51efb39fa..7b3da9c79d 100644 --- a/tools/pybind11Tools.cmake +++ b/tools/pybind11Tools.cmake @@ -37,7 +37,7 @@ endif() # A user can set versions manually too set(Python_ADDITIONAL_VERSIONS - "3.12;3.11;3.10;3.9;3.8" + "3.12;3.11;3.10;3.9" CACHE INTERNAL "") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}") @@ -100,8 +100,8 @@ set(pybind11_INCLUDE_DIRS "${pybind11_INCLUDE_DIR}" "${PYTHON_INCLUDE_DIRS}" CACHE INTERNAL "Directories where pybind11 and possibly Python headers are located") -# Python debug libraries expose slightly different objects before 3.8 -# https://docs.python.org/3.6/c-api/intro.html#debugging-builds +# A debug build of Python needs Py_DEBUG to select the matching ABI. +# https://docs.python.org/3/c-api/intro.html#debugging-builds # https://stackoverflow.com/questions/39161202/how-to-work-around-missing-pymodule-create2-in-amd64-win-python35-d-lib if(PYTHON_IS_DEBUG) set_property( From f28bd6fd7ab36b7da187cb6e7a91f6db506ba473 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 24 Jul 2026 23:52:54 -0400 Subject: [PATCH 02/10] ci(appveyor): use Python 3.9 The AppVeyor job set `PYTHON: 38`, which makes the path `C:\Python38`. The image gives Python 3.9.13 as `C:\Python39`. Assisted-by: ClaudeCode:claude-opus-5 --- .appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 391cf1071c..22c6cc3790 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -9,7 +9,7 @@ platform: - x86 environment: matrix: - - PYTHON: 38 + - PYTHON: 39 CONFIG: Debug install: - ps: | From e8935eb4d2e2e8b5c2b52c0c473ed0fedb758c03 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 25 Jul 2026 00:09:32 -0400 Subject: [PATCH 03/10] feat!: require MSVC 2019 or newer Python 3.9 is the new minimum, so MSVC 2017 is no longer needed. Raise the compile-time floor to _MSC_VER 1920 and remove the workarounds that only applied below it: std::launder, fold expressions, weak_from_this, aligned new/delete, the C4100 warning helper, and the func_handle syntax error. AppVeyor now builds with Visual Studio 2019. Assisted-by: ClaudeCode:claude-opus-5 --- .appveyor.yml | 4 ++-- docs/basics.rst | 9 ++++---- include/pybind11/detail/common.h | 25 +++++++--------------- include/pybind11/detail/type_caster_base.h | 2 +- include/pybind11/functional.h | 8 +------ include/pybind11/pybind11.h | 2 +- pybind11/setup_helpers.py | 3 +-- 7 files changed, 18 insertions(+), 35 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 22c6cc3790..43b30c6855 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,6 +1,6 @@ version: 1.0.{build} image: -- Visual Studio 2017 +- Visual Studio 2019 test: off skip_branch_with_pr: true build: @@ -13,7 +13,7 @@ environment: CONFIG: Debug install: - ps: | - $env:CMAKE_GENERATOR = "Visual Studio 15 2017" + $env:CMAKE_GENERATOR = "Visual Studio 16 2019" if ($env:PLATFORM -eq "x64") { $env:PYTHON = "$env:PYTHON-x64" } $env:PATH = "C:\Python$env:PYTHON\;C:\Python$env:PYTHON\Scripts\;$env:PATH" python -W ignore -m pip install --upgrade pip wheel diff --git a/docs/basics.rst b/docs/basics.rst index 074d98850b..7f77f201fe 100644 --- a/docs/basics.rst +++ b/docs/basics.rst @@ -32,15 +32,14 @@ The last line will both compile and run the tests. Windows ------- -On Windows, only **Visual Studio 2017** and newer are supported. +On Windows, only **Visual Studio 2019** and newer are supported. .. Note:: - To use the C++17 in Visual Studio 2017 (MSVC 14.1), pybind11 requires the flag - ``/permissive-`` to be passed to the compiler `to enforce standard conformance`_. When - building with Visual Studio 2019, this is not strictly necessary, but still advised. + The ``/permissive-`` flag `to enforce standard conformance`_ is not necessary, but it is + still advised. -.. _`to enforce standard conformance`: https://docs.microsoft.com/en-us/cpp/build/reference/permissive-standards-conformance?view=vs-2017 +.. _`to enforce standard conformance`: https://docs.microsoft.com/en-us/cpp/build/reference/permissive-standards-conformance To compile and run the tests: diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 00f2850512..7f182afb2c 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -87,7 +87,7 @@ # endif #endif -#if defined(__cpp_lib_launder) && !(defined(_MSC_VER) && (_MSC_VER < 1920)) // See PR #5968 +#if defined(__cpp_lib_launder) # define PYBIND11_STD_LAUNDER std::launder # define PYBIND11_HAS_STD_LAUNDER 1 #else @@ -132,8 +132,8 @@ # error pybind11 requires gcc 4.8 or newer # endif #elif defined(_MSC_VER) -# if _MSC_VER < 1910 -# error pybind11 2.10+ requires MSVC 2017 or newer +# if _MSC_VER < 1920 +# error pybind11 3.1+ requires MSVC 2019 or newer # endif #endif @@ -826,7 +826,7 @@ using void_t = typename void_t_impl::type; #endif /// Compile-time all/any/none of that check the boolean value of all template types -#if defined(__cpp_fold_expressions) && !(defined(_MSC_VER) && (_MSC_VER < 1916)) +#if defined(__cpp_fold_expressions) template using all_of = bool_constant<(Ts::value && ...)>; template @@ -1357,7 +1357,7 @@ try_get_shared_from_this(std::enable_shared_from_this *holder_value_ptr) { // Use at your own risk! // See also https://en.cppreference.com/w/cpp/memory/enable_shared_from_this, and in particular // the `std::shared_ptr gp1 = not_so_good.getptr();` and `try`-`catch` parts of the example. -#if defined(__cpp_lib_enable_shared_from_this) && (!defined(_MSC_VER) || _MSC_VER >= 1912) +#if defined(__cpp_lib_enable_shared_from_this) return holder_value_ptr->weak_from_this().lock(); #else try { @@ -1370,19 +1370,10 @@ try_get_shared_from_this(std::enable_shared_from_this *holder_value_ptr) { // For silencing "unused" compiler warnings in special situations. template -#if defined(_MSC_VER) && _MSC_VER < 1920 // MSVC 2017 -constexpr -#endif - inline void silence_unused_warnings(Args &&...) { -} +inline void silence_unused_warnings(Args &&...) {} -// MSVC warning C4100: Unreferenced formal parameter -#if defined(_MSC_VER) && _MSC_VER <= 1916 -# define PYBIND11_WORKAROUND_INCORRECT_MSVC_C4100(...) \ - detail::silence_unused_warnings(__VA_ARGS__) -#else -# define PYBIND11_WORKAROUND_INCORRECT_MSVC_C4100(...) -#endif +// MSVC warning C4100: Unreferenced formal parameter (only incorrect before MSVC 2019) +#define PYBIND11_WORKAROUND_INCORRECT_MSVC_C4100(...) // GCC -Wunused-but-set-parameter All GCC versions (as of July 2021). #if defined(__GNUG__) && !defined(__clang__) && !defined(__INTEL_COMPILER) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 8fbf700e12..3b444ef600 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -1133,7 +1133,7 @@ class type_caster_generic { if (type->operator_new) { vptr = type->operator_new(type->type_size); } else { -#if defined(__cpp_aligned_new) && (!defined(_MSC_VER) || _MSC_VER >= 1912) +#if defined(__cpp_aligned_new) if (type->type_align > __STDCPP_DEFAULT_NEW_ALIGNMENT__) { vptr = ::operator new(type->type_size, std::align_val_t(type->type_align)); } else { diff --git a/include/pybind11/functional.h b/include/pybind11/functional.h index c7122795ad..c06fabe0d6 100644 --- a/include/pybind11/functional.h +++ b/include/pybind11/functional.h @@ -20,13 +20,7 @@ PYBIND11_NAMESPACE_BEGIN(type_caster_std_function_specializations) // ensure GIL is held during functor destruction struct func_handle { function f; -#if !(defined(_MSC_VER) && _MSC_VER == 1916 && defined(PYBIND11_CPP17)) - // This triggers a syntax error under very special conditions (very weird indeed). - explicit -#endif - func_handle(function &&f_) noexcept - : f(std::move(f_)) { - } + explicit func_handle(function &&f_) noexcept : f(std::move(f_)) {} func_handle(const func_handle &f_) { operator=(f_); } func_handle &operator=(const func_handle &f_) { gil_scoped_acquire acq; diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 159ecd28e7..b62b6e79e2 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1958,7 +1958,7 @@ void call_operator_delete(T *p, size_t s, size_t) { inline void call_operator_delete(void *p, size_t s, size_t a) { (void) s; (void) a; -#if defined(__cpp_aligned_new) && (!defined(_MSC_VER) || _MSC_VER >= 1912) +#if defined(__cpp_aligned_new) if (a > __STDCPP_DEFAULT_NEW_ALIGNMENT__) { # ifdef __cpp_sized_deallocation ::operator delete(p, s, std::align_val_t(a)); diff --git a/pybind11/setup_helpers.py b/pybind11/setup_helpers.py index c4083756c6..8f42605245 100644 --- a/pybind11/setup_helpers.py +++ b/pybind11/setup_helpers.py @@ -172,8 +172,7 @@ def cxx_std(self, level: int) -> None: "You cannot safely change the cxx_level after setting it!", stacklevel=2 ) - # MSVC 2015 Update 3 and later only have 14 (and later 17) modes, so - # force a valid flag here. + # MSVC only has 14 and later modes, so force a valid flag here. if WIN and level == 11: level = 14 From e883fdbc204710818fbf84c2de7690681407caba Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 25 Jul 2026 00:44:30 -0400 Subject: [PATCH 04/10] fix(appveyor): build against the Python that has the test packages CMake 4 has no FindPythonLibs, so pybind11 uses FindPython. FindPython reads the registry before PATH and selected `C:\Python314-x64`, but the test packages go into the Python on PATH. Pass `Python_ROOT_DIR` to name the correct one. Also set `CMAKE_ARCH`. It was never set, so the architecture came from the generator. Visual Studio 2017 defaults to Win32, but Visual Studio 2019 defaults to x64, which made this x86 job build 64-bit code. Assisted-by: ClaudeCode:claude-opus-5 --- .appveyor.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 43b30c6855..8ba9dc014e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -14,8 +14,15 @@ environment: install: - ps: | $env:CMAKE_GENERATOR = "Visual Studio 16 2019" - if ($env:PLATFORM -eq "x64") { $env:PYTHON = "$env:PYTHON-x64" } - $env:PATH = "C:\Python$env:PYTHON\;C:\Python$env:PYTHON\Scripts\;$env:PATH" + # The 2019 generator defaults to x64, so name the architecture explicitly. + if ($env:PLATFORM -eq "x64") { + $env:PYTHON = "$env:PYTHON-x64" + $env:CMAKE_ARCH = "x64" + } else { + $env:CMAKE_ARCH = "Win32" + } + $env:PYTHON_ROOT = "C:\Python$env:PYTHON" + $env:PATH = "$env:PYTHON_ROOT\;$env:PYTHON_ROOT\Scripts\;$env:PATH" python -W ignore -m pip install --upgrade pip wheel python -W ignore -m pip install pytest numpy --no-warn-script-location pytest-timeout - ps: | @@ -23,7 +30,10 @@ install: 7z x eigen-3.3.7.zip -y > $null $env:CMAKE_INCLUDE_PATH = "eigen-3.3.7;$env:CMAKE_INCLUDE_PATH" build_script: +# FindPython looks at the registry before PATH, so it picks the newest Python on +# the image. Point it at the one that has the test packages. - cmake -G "%CMAKE_GENERATOR%" -A "%CMAKE_ARCH%" + -DPython_ROOT_DIR="%PYTHON_ROOT%" -DCMAKE_CXX_STANDARD=14 -DPYBIND11_WERROR=ON -DDOWNLOAD_CATCH=ON From c9455a46dd1e3820d7e202dadf055ca060407447 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 25 Jul 2026 09:05:36 -0400 Subject: [PATCH 05/10] fix(appveyor): give the linker Python's libs directory The build compiled but failed to link with LNK1104 on a bare `python39.lib`. That name comes from the `#pragma comment(lib, ...)` in pyconfig.h, so the linker needs the directory. CMake 4 has no FindPythonLibs, and FindPython does not add it for this Debug x86 build. Put it on LIB instead. The directory listing is temporary, to confirm the library is present. Assisted-by: ClaudeCode:claude-opus-5 --- .appveyor.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.appveyor.yml b/.appveyor.yml index 8ba9dc014e..7e2c51b4e7 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -23,6 +23,10 @@ install: } $env:PYTHON_ROOT = "C:\Python$env:PYTHON" $env:PATH = "$env:PYTHON_ROOT\;$env:PYTHON_ROOT\Scripts\;$env:PATH" + # pyconfig.h names python39.lib in a #pragma comment(lib), so the linker must + # know the directory. FindPython does not add it here. + $env:LIB = "$env:PYTHON_ROOT\libs;$env:LIB" + Get-ChildItem "$env:PYTHON_ROOT\libs" python -W ignore -m pip install --upgrade pip wheel python -W ignore -m pip install pytest numpy --no-warn-script-location pytest-timeout - ps: | From c9b03e470102991b7d67d11d9812779937ea13f0 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 25 Jul 2026 09:08:46 -0400 Subject: [PATCH 06/10] fix(appveyor): link the release Python library in Debug The image ships python39_d.lib next to python39.lib, so FindPython picks the debug import library for a Debug build. pybind11 undefines _DEBUG around Python.h, so pyconfig.h asks for python39.lib instead and the link failed with LNK1104. Name the release library for the debug slot. Setting LIB does not work, because MSBuild replaces it from the toolset, and it would link both import libraries. Assisted-by: ClaudeCode:claude-opus-5 --- .appveyor.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 7e2c51b4e7..d3e71ae6d2 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -14,6 +14,7 @@ environment: install: - ps: | $env:CMAKE_GENERATOR = "Visual Studio 16 2019" + $env:PYTHON_LIB_NAME = "python$env:PYTHON.lib" # The 2019 generator defaults to x64, so name the architecture explicitly. if ($env:PLATFORM -eq "x64") { $env:PYTHON = "$env:PYTHON-x64" @@ -22,11 +23,8 @@ install: $env:CMAKE_ARCH = "Win32" } $env:PYTHON_ROOT = "C:\Python$env:PYTHON" + $env:PYTHON_LIB = "$env:PYTHON_ROOT\libs\$env:PYTHON_LIB_NAME" $env:PATH = "$env:PYTHON_ROOT\;$env:PYTHON_ROOT\Scripts\;$env:PATH" - # pyconfig.h names python39.lib in a #pragma comment(lib), so the linker must - # know the directory. FindPython does not add it here. - $env:LIB = "$env:PYTHON_ROOT\libs;$env:LIB" - Get-ChildItem "$env:PYTHON_ROOT\libs" python -W ignore -m pip install --upgrade pip wheel python -W ignore -m pip install pytest numpy --no-warn-script-location pytest-timeout - ps: | @@ -36,8 +34,12 @@ install: build_script: # FindPython looks at the registry before PATH, so it picks the newest Python on # the image. Point it at the one that has the test packages. +# +# The image also has python39_d.lib, which FindPython selects for a Debug build. +# pybind11 undefines _DEBUG around Python.h, so it needs the release library. - cmake -G "%CMAKE_GENERATOR%" -A "%CMAKE_ARCH%" -DPython_ROOT_DIR="%PYTHON_ROOT%" + -DPython_LIBRARY_DEBUG="%PYTHON_LIB%" -DCMAKE_CXX_STANDARD=14 -DPYBIND11_WERROR=ON -DDOWNLOAD_CATCH=ON From 880174e4dcb7166273ecb2ca2aa96522203467a6 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 25 Jul 2026 09:43:12 -0400 Subject: [PATCH 07/10] chore(appveyor): print link settings to debug LNK1104 Revert the two attempted fixes. Neither changed the failure: setting LIB does not survive MSBuild, and naming the release library for Python_LIBRARY_DEBUG had no effect. Print the Python cache entries and the link settings of a generated project file instead, to see what the linker really gets. Temporary. Assisted-by: ClaudeCode:claude-opus-5 --- .appveyor.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index d3e71ae6d2..72a08ffb48 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -14,7 +14,6 @@ environment: install: - ps: | $env:CMAKE_GENERATOR = "Visual Studio 16 2019" - $env:PYTHON_LIB_NAME = "python$env:PYTHON.lib" # The 2019 generator defaults to x64, so name the architecture explicitly. if ($env:PLATFORM -eq "x64") { $env:PYTHON = "$env:PYTHON-x64" @@ -23,7 +22,6 @@ install: $env:CMAKE_ARCH = "Win32" } $env:PYTHON_ROOT = "C:\Python$env:PYTHON" - $env:PYTHON_LIB = "$env:PYTHON_ROOT\libs\$env:PYTHON_LIB_NAME" $env:PATH = "$env:PYTHON_ROOT\;$env:PYTHON_ROOT\Scripts\;$env:PATH" python -W ignore -m pip install --upgrade pip wheel python -W ignore -m pip install pytest numpy --no-warn-script-location pytest-timeout @@ -34,17 +32,18 @@ install: build_script: # FindPython looks at the registry before PATH, so it picks the newest Python on # the image. Point it at the one that has the test packages. -# -# The image also has python39_d.lib, which FindPython selects for a Debug build. -# pybind11 undefines _DEBUG around Python.h, so it needs the release library. - cmake -G "%CMAKE_GENERATOR%" -A "%CMAKE_ARCH%" -DPython_ROOT_DIR="%PYTHON_ROOT%" - -DPython_LIBRARY_DEBUG="%PYTHON_LIB%" -DCMAKE_CXX_STANDARD=14 -DPYBIND11_WERROR=ON -DDOWNLOAD_CATCH=ON -DCMAKE_SUPPRESS_REGENERATION=1 . +# Temporary, to find why the linker gets python39.lib without a directory. +- ps: | + cmake -N -LA . | Select-String -Pattern "^Python" + Get-ChildItem "$env:PYTHON_ROOT\libs" + Select-String -Path "tests\pybind11_tests.vcxproj" -Pattern "AdditionalDependencies|AdditionalLibraryDirectories" - set MSBuildLogger="C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" - cmake --build . --config %CONFIG% --target pytest -- /m /v:m /logger:%MSBuildLogger% - cmake --build . --config %CONFIG% --target cpptest -- /m /v:m /logger:%MSBuildLogger% From 62b8db1fa98e1c5a2bfaa61c912d925b8e0abb0a Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 25 Jul 2026 09:56:16 -0400 Subject: [PATCH 08/10] fix(appveyor): take the release Python library in Debug The generated project file linked C:\Python39\libs\python39_d.lib in the Debug configuration, because the image ships debug binaries next to the release ones. pybind11 undefines _DEBUG around Python.h, so pyconfig.h asks for python39.lib in a #pragma comment(lib), which nothing on the link line satisfies and no library directory holds. Map Debug to the release artifacts. Assisted-by: ClaudeCode:claude-opus-5 --- .appveyor.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.appveyor.yml b/.appveyor.yml index 72a08ffb48..4b14e5bfc9 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -32,8 +32,13 @@ install: build_script: # FindPython looks at the registry before PATH, so it picks the newest Python on # the image. Point it at the one that has the test packages. +# +# The image also has python39_d.lib, which FindPython uses for a Debug build. +# pybind11 undefines _DEBUG around Python.h, so pyconfig.h asks for python39.lib +# and the link fails. Take the release library in Debug too. - cmake -G "%CMAKE_GENERATOR%" -A "%CMAKE_ARCH%" -DPython_ROOT_DIR="%PYTHON_ROOT%" + -DCMAKE_MAP_IMPORTED_CONFIG_DEBUG=Release -DCMAKE_CXX_STANDARD=14 -DPYBIND11_WERROR=ON -DDOWNLOAD_CATCH=ON From 2d3c1c2cec2b5c986a679c12d74840cf17655038 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 25 Jul 2026 10:08:57 -0400 Subject: [PATCH 09/10] chore(appveyor): drop the temporary link diagnostic Assisted-by: ClaudeCode:claude-opus-5 --- .appveyor.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 4b14e5bfc9..8429d5ec57 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -44,11 +44,6 @@ build_script: -DDOWNLOAD_CATCH=ON -DCMAKE_SUPPRESS_REGENERATION=1 . -# Temporary, to find why the linker gets python39.lib without a directory. -- ps: | - cmake -N -LA . | Select-String -Pattern "^Python" - Get-ChildItem "$env:PYTHON_ROOT\libs" - Select-String -Path "tests\pybind11_tests.vcxproj" -Pattern "AdditionalDependencies|AdditionalLibraryDirectories" - set MSBuildLogger="C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" - cmake --build . --config %CONFIG% --target pytest -- /m /v:m /logger:%MSBuildLogger% - cmake --build . --config %CONFIG% --target cpptest -- /m /v:m /logger:%MSBuildLogger% From 6e22547fe30caa9968ab0d4a6275d6cf091f9e08 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sun, 26 Jul 2026 00:54:33 -0400 Subject: [PATCH 10/10] fix(tests): guard the unraisable warning filter for pytest < 6 The distro pytest in the Clang and GCC Docker jobs has no PytestUnraisableExceptionWarning, so an unconditional filterwarnings mark makes pytest fail with an INTERNALERROR after the tests pass. Assisted-by: ClaudeCode:claude-opus-5 Claude-Session: https://claude.ai/code/session_01Aimf6HuSz1vLRwBnbxmCTc --- tests/test_exceptions.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 1c70a9ea6e..f811cbce12 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -92,9 +92,17 @@ def test_python_call_in_catch(): assert d["good"] is True +def ignore_pytest_unraisable_warning(f): + unraisable = "PytestUnraisableExceptionWarning" + if hasattr(pytest, unraisable): # pytest >= 6 + dec = pytest.mark.filterwarnings(f"ignore::pytest.{unraisable}") + return dec(f) + return f + + # TODO: find out why this fails on PyPy, https://foss.heptapod.net/pypy/pypy/-/issues/3583 @pytest.mark.xfail(env.PYPY, reason="Failure on PyPy 3.8 (7.3.7)", strict=False) -@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") +@ignore_pytest_unraisable_warning def test_python_alreadyset_in_destructor(monkeypatch, capsys): triggered = False