chore(deps): update pre-commit hooks (#6126)

* chore(deps): update pre-commit hooks

updates:
- [github.com/pre-commit/mirrors-clang-format: v22.1.5 → v22.1.8](https://github.com/pre-commit/mirrors-clang-format/compare/v22.1.5...v22.1.8)
- [github.com/astral-sh/ruff-pre-commit: v0.15.20 → v0.16.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.20...v0.16.1)
- [github.com/pre-commit/mirrors-mypy: v2.1.0 → v2.3.0](https://github.com/pre-commit/mirrors-mypy/compare/v2.1.0...v2.3.0)
- [github.com/codespell-project/codespell: v2.4.2 → v2.4.3](https://github.com/codespell-project/codespell/compare/v2.4.2...v2.4.3)

* style: pre-commit fixes

* fix: adapt to the ruff 0.16 default rule set

The hook update pulled in ruff 0.16, which expanded its default rules and
autofixed `self: S -> S` to `-> Self`. That fix added a runtime
`typing_extensions` import to setup_helpers.py, which must stay
dependency-free; it broke every test job whose environment does not supply
that package. The import is now guarded by TYPE_CHECKING.

Also replace the `exec` in docs/conf.py with an importlib module load,
parenthesize implicit string concatenations in list literals, mark unused
unpacked variables with a leading underscore, and make noxfile.py
executable to match its shebang.

The remaining new rules are silenced per-file, with reasons: the chrono
tests check naive local time on purpose, and the other rules conflict with
how the tests are written.

Assisted-by: ClaudeCode:claude-opus-5

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Henry Schreiner <henryfs@princeton.edu>
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index aff6954..1934c2d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -25,14 +25,14 @@
 
 # Clang format the codebase automatically
 - repo: https://github.com/pre-commit/mirrors-clang-format
-  rev: "v22.1.5"
+  rev: "v22.1.8"
   hooks:
   - id: clang-format
     types_or: [c++, c, cuda]
 
 # Ruff, the Python auto-correcting linter/formatter written in Rust
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.15.20
+  rev: v0.16.1
   hooks:
   - id: ruff-check
     args: ["--fix", "--show-fixes"]
@@ -40,7 +40,7 @@
 
 # Check static types with mypy
 - repo: https://github.com/pre-commit/mirrors-mypy
-  rev: "v2.1.0"
+  rev: "v2.3.0"
   hooks:
   - id: mypy
     args: []
@@ -112,7 +112,7 @@
 # Use tools/codespell_ignore_lines_from_errors.py
 # to rebuild .codespell-ignore-lines
 - repo: https://github.com/codespell-project/codespell
-  rev: "v2.4.2"
+  rev: "v2.4.3"
   hooks:
   - id: codespell
     exclude: "(.supp|^pyproject.toml)$"
diff --git a/docs/conf.py b/docs/conf.py
index 5f216bf..481e04c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,5 +1,3 @@
-#!/usr/bin/env python3
-#
 # pybind11 documentation build configuration file, created by
 # sphinx-quickstart on Sun Oct 11 19:23:48 2015.
 #
@@ -13,6 +11,7 @@
 # serve to show the default.
 from __future__ import annotations
 
+import importlib.util
 import os
 import re
 import subprocess
@@ -69,13 +68,14 @@
 
 # Read the listed version
 version_file = DIR.parent / "pybind11/_version.py"
-with version_file.open(encoding="utf-8") as f:
-    code = compile(f.read(), version_file, "exec")
-loc = {"__file__": str(version_file)}
-exec(code, loc)
+spec = importlib.util.spec_from_file_location("pybind11_version", version_file)
+assert spec is not None
+assert spec.loader is not None
+version_module = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(version_module)
 
 # The full version, including alpha/beta/rc tags.
-version = loc["__version__"]
+version = version_module.__version__
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/noxfile.py b/noxfile.py
old mode 100644
new mode 100755
diff --git a/pybind11/__init__.py b/pybind11/__init__.py
index df5e8ee..3882b2b 100644
--- a/pybind11/__init__.py
+++ b/pybind11/__init__.py
@@ -11,9 +11,9 @@
 from .commands import get_cmake_dir, get_include, get_pkgconfig_dir
 
 __all__ = (
-    "version_info",
     "__version__",
-    "get_include",
     "get_cmake_dir",
+    "get_include",
     "get_pkgconfig_dir",
+    "version_info",
 )
diff --git a/pybind11/setup_helpers.py b/pybind11/setup_helpers.py
index 8f42605..66a3eec 100644
--- a/pybind11/setup_helpers.py
+++ b/pybind11/setup_helpers.py
@@ -52,10 +52,10 @@
 from functools import lru_cache
 from pathlib import Path
 from typing import (
+    TYPE_CHECKING,
     Any,
     Callable,
     Optional,
-    TypeVar,
     Union,
 )
 
@@ -71,6 +71,9 @@
 import distutils.ccompiler
 import distutils.errors
 
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
 WIN = sys.platform.startswith("win32") and "mingw" not in sysconfig.get_platform()
 MACOS = sys.platform.startswith("darwin")
 STD_TMPL = "/std:c++{}" if WIN else "-std=c++{}"
@@ -338,8 +341,6 @@
     return True
 
 
-S = TypeVar("S", bound="ParallelCompile")
-
 CCompilerMethod = Callable[
     [
         distutils.ccompiler.CCompiler,
@@ -397,7 +398,7 @@
     called.
     """
 
-    __slots__ = ("envvar", "default", "max", "_old", "needs_recompile")
+    __slots__ = ("_old", "default", "envvar", "max", "needs_recompile")
 
     def __init__(
         self,
@@ -477,16 +478,16 @@
 
         return compile_function
 
-    def install(self: S) -> S:
+    def install(self) -> Self:
         """
         Installs the compile function into distutils.ccompiler.CCompiler.compile.
         """
         distutils.ccompiler.CCompiler.compile = self.function()  # type: ignore[assignment]
         return self
 
-    def __enter__(self: S) -> S:
+    def __enter__(self) -> Self:
         self._old.append(distutils.ccompiler.CCompiler.compile)
         return self.install()
 
-    def __exit__(self, *args: Any) -> None:
+    def __exit__(self, *args: object) -> None:
         distutils.ccompiler.CCompiler.compile = self._old.pop()  # type: ignore[assignment]
diff --git a/pyproject.toml b/pyproject.toml
index caf0a8b..cb5d728 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -181,8 +181,14 @@
   "EM",
   "N",
   "E721",
+  "BLE001", # Tests capture and re-report exceptions from workers and callbacks
+  "DTZ",    # test_chrono.py checks naive local-time round-tripping on purpose
+  "FLY002", # Joining a list keeps long signatures one-per-line
+  "RUF012", # ClassVar annotations are noise in test fixtures
+  "RUF063", # test_pytypes.py reads __annotations__ from __dict__ deliberately
 ]
 "tests/test_call_policies.py" = ["PLC1901"]
+"docs/benchmark.py" = ["DTZ"]
 
 [tool.repo-review]
 ignore = ["PP"]
diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py
index e2c1856..164611d 100644
--- a/tests/extra_python_package/test_files.py
+++ b/tests/extra_python_package/test_files.py
@@ -25,7 +25,7 @@
 # Newer pytest has global path setting, but keeping old pytest for now
 sys.path.append(str(MAIN_DIR / "tools"))
 
-from make_global import get_global  # noqa: E402
+from make_global import get_global
 
 HAS_UV = shutil.which("uv") is not None
 UV_ARGS = ["--installer=uv"] if HAS_UV else []
diff --git a/tests/test_enum.py b/tests/test_enum.py
index 53dcc09..81170c9 100644
--- a/tests/test_enum.py
+++ b/tests/test_enum.py
@@ -71,8 +71,8 @@
     assert y != 3
     assert 3 != y
     # Compare with None
-    assert y != None  # noqa: E711
-    assert not (y == None)  # noqa: E711
+    assert y != None
+    assert not (y == None)
     # Compare with an object
     assert y != object()
     assert not (y == object())
@@ -137,8 +137,8 @@
     assert z != 3
     assert 3 != z
     # Compare with None
-    assert z != None  # noqa: E711
-    assert not (z == None)  # noqa: E711
+    assert z != None
+    assert not (z == None)
     # Compare with an object
     assert z != object()
     assert not (z == object())
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index c52a295..aeea5d8 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -112,7 +112,7 @@
     default_hook = sys.__unraisablehook__
 
     def hook(unraisable_hook_args):
-        exc_type, exc_value, exc_tb, err_msg, obj = unraisable_hook_args
+        _exc_type, _exc_value, _exc_tb, _err_msg, obj = unraisable_hook_args
         if obj == "already_set demo":
             nonlocal triggered
             triggered = True
@@ -344,8 +344,10 @@
     lines = str(excinfo.value).splitlines()
     # PyErr_NormalizeException replaces the original FlakyException with ValueError:
     assert lines[:3] == [
-        "pybind11::error_already_set: MISMATCH of original and normalized active exception types:"
-        " ORIGINAL FlakyException REPLACED BY ValueError: triggered_failure_point_init",
+        (
+            "pybind11::error_already_set: MISMATCH of original and normalized active exception types:"
+            " ORIGINAL FlakyException REPLACED BY ValueError: triggered_failure_point_init"
+        ),
         "",
         "At:",
     ]
diff --git a/tests/test_iostream.py b/tests/test_iostream.py
index 857e0b5..8b11997 100644
--- a/tests/test_iostream.py
+++ b/tests/test_iostream.py
@@ -160,16 +160,16 @@
 
     with m.ostream_redirect():
         m.noisy_function(msg, flush=False)
-        stdout, stderr = capfd.readouterr()
+        stdout, _stderr = capfd.readouterr()
         assert not stdout
 
         m.noisy_function(msg2, flush=True)
-        stdout, stderr = capfd.readouterr()
+        stdout, _stderr = capfd.readouterr()
         assert stdout == msg + msg2
 
         m.noisy_function(msg, flush=False)
 
-    stdout, stderr = capfd.readouterr()
+    stdout, _stderr = capfd.readouterr()
     assert stdout == msg
 
 
@@ -218,7 +218,7 @@
         m.raw_output("b")
         m.captured_output("c")
         m.raw_output("d")
-    stdout, stderr = capfd.readouterr()
+    stdout, _stderr = capfd.readouterr()
     assert stdout == "bd"
     assert stream.getvalue() == "ac"
 
@@ -235,21 +235,21 @@
     stream = StringIO()
     with redirect_stdout(stream):
         m.raw_output(msg)
-    stdout, stderr = capfd.readouterr()
+    stdout, _stderr = capfd.readouterr()
     assert stdout == msg
     assert not stream.getvalue()
 
     stream = StringIO()
     with redirect_stdout(stream), m.ostream_redirect():
         m.raw_output(msg)
-    stdout, stderr = capfd.readouterr()
+    stdout, _stderr = capfd.readouterr()
     assert not stdout
     assert stream.getvalue() == msg
 
     stream = StringIO()
     with redirect_stdout(stream):
         m.raw_output(msg)
-    stdout, stderr = capfd.readouterr()
+    stdout, _stderr = capfd.readouterr()
     assert stdout == msg
     assert not stream.getvalue()
 
diff --git a/tests/test_numpy_dtypes.py b/tests/test_numpy_dtypes.py
index 22814ab..ba45d8b 100644
--- a/tests/test_numpy_dtypes.py
+++ b/tests/test_numpy_dtypes.py
@@ -316,12 +316,18 @@
         "'offsets':[0,12,20,24],'itemsize':56}"
     )
     assert m.print_array_array(arr) == [
-        "a={{A,B,C,D},{K,L,M,N},{U,V,W,X}},b={0,1},"
-        "c={0,1,2},d={{0,1},{10,11},{20,21},{30,31}}",
-        "a={{W,X,Y,Z},{G,H,I,J},{Q,R,S,T}},b={1000,1001},"
-        "c={10,11,12},d={{100,101},{110,111},{120,121},{130,131}}",
-        "a={{S,T,U,V},{C,D,E,F},{M,N,O,P}},b={2000,2001},"
-        "c={20,21,22},d={{200,201},{210,211},{220,221},{230,231}}",
+        (
+            "a={{A,B,C,D},{K,L,M,N},{U,V,W,X}},b={0,1},"
+            "c={0,1,2},d={{0,1},{10,11},{20,21},{30,31}}"
+        ),
+        (
+            "a={{W,X,Y,Z},{G,H,I,J},{Q,R,S,T}},b={1000,1001},"
+            "c={10,11,12},d={{100,101},{110,111},{120,121},{130,131}}"
+        ),
+        (
+            "a={{S,T,U,V},{C,D,E,F},{M,N,O,P}},b={2000,2001},"
+            "c={20,21,22},d={{200,201},{210,211},{220,221},{230,231}}"
+        ),
     ]
     assert arr["a"].tolist() == [
         [b"ABCD", b"KLMN", b"UVWX"],
diff --git a/tests/test_smart_ptr.py b/tests/test_smart_ptr.py
index 7ee4b78..326b768 100644
--- a/tests/test_smart_ptr.py
+++ b/tests/test_smart_ptr.py
@@ -5,7 +5,7 @@
 import env  # noqa: F401
 
 m = pytest.importorskip("pybind11_tests.smart_ptr")
-from pybind11_tests import ConstructorStats  # noqa: E402
+from pybind11_tests import ConstructorStats
 
 
 @pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
diff --git a/tests/test_stl.py b/tests/test_stl.py
index c75ccb8..f3f4ccc 100644
--- a/tests/test_stl.py
+++ b/tests/test_stl.py
@@ -818,7 +818,7 @@
 
 
 def test_set_caster_protocol(doc):
-    from collections.abc import Set
+    from collections.abc import Set as AbstractSet
 
     # Implements the Set protocol without explicitly inheriting from collections.abc.Set.
     class BareSetLike:
@@ -836,7 +836,7 @@
 
     # Implements the Set protocol by reusing BareSetLike's implementation.
     # Additionally, inherits from collections.abc.Set.
-    class FormalSetLike(BareSetLike, Set):
+    class FormalSetLike(BareSetLike, AbstractSet):
         pass
 
     # convert mode
diff --git a/tests/test_virtual_functions.py b/tests/test_virtual_functions.py
index 617c87b..c2bba47 100644
--- a/tests/test_virtual_functions.py
+++ b/tests/test_virtual_functions.py
@@ -7,7 +7,7 @@
 import env
 
 m = pytest.importorskip("pybind11_tests.virtual_functions")
-from pybind11_tests import ConstructorStats  # noqa: E402
+from pybind11_tests import ConstructorStats
 
 
 def test_override(capture, msg):
diff --git a/tools/make_changelog.py b/tools/make_changelog.py
index f872546..8d2c7b0 100755
--- a/tools/make_changelog.py
+++ b/tools/make_changelog.py
@@ -94,8 +94,7 @@
     if not msg:
         missing.append(issue)
         continue
-    if msg.startswith("* "):
-        msg = msg[2:]
+    msg = msg.removeprefix("* ")
     if not msg.startswith("- "):
         msg = "- " + msg
     if not msg.endswith("."):