ci: test Python 3.15 (#6107)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d7e9e86..2ad1178 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -53,6 +53,9 @@
           - runs-on: ubuntu-latest
             python-version: 'graalpy-25.0'
             cmake-args: -DCMAKE_CXX_STANDARD=20
+          - runs-on: ubuntu-latest
+            python-version: '3.15'
+            cmake-args: -DCMAKE_CXX_STANDARD=20
           - runs-on: macos-latest
             python-version: '3.14'
             cmake-args: -DCMAKE_CXX_STANDARD=14
@@ -86,6 +89,9 @@
           - runs-on: ubuntu-latest
             python-version: '3.14'
             cmake-args: -DCMAKE_CXX_STANDARD=14
+          - runs-on: ubuntu-latest
+            python-version: '3.15t'
+            cmake-args: -DCMAKE_CXX_STANDARD=17 -DPYBIND11_TEST_SMART_HOLDER=ON
 
           # No SciPy for macOS ARM
           - runs-on: macos-15-intel
@@ -101,6 +107,9 @@
             python-version: '3.14t'
             cmake-args: -DCMAKE_CXX_STANDARD=20
           - runs-on: macos-latest
+            python-version: '3.15'
+            cmake-args: -DCMAKE_CXX_STANDARD=17
+          - runs-on: macos-latest
             python-version: 'pypy-3.11-v7.3.23'
           - runs-on: macos-latest
             python-version: 'graalpy-25.0'
@@ -128,6 +137,9 @@
             python-version: '3.14t'
             cmake-args: -DCMAKE_CXX_STANDARD=23
           - runs-on: windows-latest
+            python-version: '3.15'
+            cmake-args: -DCMAKE_CXX_STANDARD=20
+          - runs-on: windows-latest
             python-version: 'pypy3.11'
             cmake-args: -DCMAKE_CXX_STANDARD=20
           # The setup-python action currently doesn't have graalpy for windows
diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml
index 9639d26..d27af50 100644
--- a/.github/workflows/upstream.yml
+++ b/.github/workflows/upstream.yml
@@ -18,7 +18,7 @@
 
 jobs:
   standard:
-    name: "🐍 3.13 latest • ubuntu-latest • x64"
+    name: "🐍 3.15 latest • ubuntu-latest • x64"
     runs-on: ubuntu-latest
     # Only runs when the  'python dev' label is selected
     if: "contains(github.event.pull_request.labels.*.name, 'python dev')"
@@ -26,10 +26,10 @@
     steps:
     - uses: actions/checkout@v7
 
-    - name: Setup Python 3.13
+    - name: Setup Python 3.15
       uses: actions/setup-python@v6
       with:
-        python-version: "3.13"
+        python-version: "3.15"
         allow-prereleases: true
 
     - name: Setup Boost
diff --git a/pyproject.toml b/pyproject.toml
index 03214c6..c3395aa 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -23,6 +23,7 @@
     "Programming Language :: Python :: 3.12",
     "Programming Language :: Python :: 3.13",
     "Programming Language :: Python :: 3.14",
+    "Programming Language :: Python :: 3.15",
     "Programming Language :: Python :: Implementation :: PyPy",
     "Programming Language :: Python :: Implementation :: CPython",
     "Programming Language :: C++",
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 53f05b6..1ba4026 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -1,6 +1,8 @@
 --extra-index-url=https://www.graalvm.org/python/wheels
 --only-binary=:all:
 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"
@@ -10,7 +12,6 @@
 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"
 numpy~=2.2.0; platform_python_implementation=="CPython" and python_version=="3.13" and platform_machine!="ARM64"
-numpy>=2.4.0; platform_python_implementation=="CPython" and python_version>="3.14"
 pytest>=6
 pytest-timeout
 scipy~=1.5.4; platform_python_implementation=="CPython" and python_version<"3.10"
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index e6441f6..1ee13ba 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import re
 import sys
 
 import pytest
@@ -123,6 +124,8 @@
     assert triggered is True
 
     _, captured_stderr = capsys.readouterr()
+    # Strip ANSI codes that Python 3.15+ adds to this traceback under FORCE_COLOR.
+    captured_stderr = re.sub(r"\x1b\[[0-?]*[ -/]*m", "", captured_stderr)
     assert captured_stderr.startswith("Exception ignored in: 'already_set demo'")
     assert captured_stderr.rstrip().endswith("KeyError: 'bar'")