From: Roland Mas <roland.mas@entierement.net>
Date: Fri, 2 Jan 2026 14:13:13 +0100
Subject: Python 3.14 compatibility

Partial backport of upstream's pull request at
https://github.com/pyapp-kit/magicgui/pull/721
---
 .github/workflows/deploy_docs.yml     |  1 +
 .github/workflows/test_and_deploy.yml | 10 +++++-----
 pyproject.toml                        |  7 +++++--
 src/magicgui/signature.py             | 17 +++++++++++------
 tests/conftest.py                     | 12 ++++++++++++
 5 files changed, 34 insertions(+), 13 deletions(-)

diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml
index f8f3ad2..7767233 100644
--- a/.github/workflows/deploy_docs.yml
+++ b/.github/workflows/deploy_docs.yml
@@ -17,6 +17,7 @@ jobs:
           fetch-depth: 0
       - uses: astral-sh/setup-uv@v6
         with:
+          python-version: "3.13"
           enable-cache: true
 
       - name: Deploy docs to GitHub Pages
diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml
index d20e606..bf007d2 100644
--- a/.github/workflows/test_and_deploy.yml
+++ b/.github/workflows/test_and_deploy.yml
@@ -23,7 +23,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["3.10", "3.11"]
+        python-version: ["3.10", "3.13"]
         os: [ubuntu-latest, macos-latest, windows-latest]
         add-group: [pyqt6, pyside6]
         include:
@@ -37,11 +37,11 @@ jobs:
           - python-version: "3.9"
             os: ubuntu-latest
             add-group: pyside2
-          - python-version: "3.11"
+          - python-version: "3.12"
             os: windows-latest
             add-group: pyqt5
           - python-version: "3.10"
-            os: ubuntu-latest
+            os: windows-latest
             add-group: pyside2
           - python-version: "3.12"
             os: ubuntu-latest
@@ -49,10 +49,10 @@ jobs:
           - python-version: "3.12"
             os: ubuntu-latest
             add-group: pyside6
-          - python-version: "3.13"
+          - python-version: "3.14"
             os: ubuntu-latest
             add-group: pyside6
-          - python-version: "3.13"
+          - python-version: "3.14"
             os: windows-latest
             add-group: pyqt6
     steps:
diff --git a/pyproject.toml b/pyproject.toml
index bfb954c..ad3b82e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,6 +28,7 @@ classifiers = [
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
     "Programming Language :: Python :: 3.13",
+    "Programming Language :: Python :: 3.14",
     "Topic :: Desktop Environment",
     "Topic :: Software Development",
     "Topic :: Software Development :: User Interfaces",
@@ -65,6 +66,7 @@ third-party-support = [
     "attrs>=25.3.0",
     "ipykernel>=6.29.5",
     "matplotlib>=3.9.4",
+    "numpy>=2.1.0; python_version >= '3.13'",
     "numpy>=1.26.4",
     "pandas>=2.2.3; python_version >= '3.11'",
     "pandas>=2.1",
@@ -81,7 +83,7 @@ test = [
 test-qt = [{ include-group = "test" }, "pytest-qt >=4.3.0"]
 pyqt5 = ["magicgui[pyqt5]", { include-group = "test-qt" }]
 pyqt6 = ["magicgui[pyqt6]", { include-group = "test-qt" }]
-pyside2 = ["magicgui[pyside2]", { include-group = "test-qt" }]
+pyside2 = ["magicgui[pyside2]", { include-group = "test-qt" }, "numpy<2; python_version < '3.13'"]
 pyside6 = ["magicgui[pyside6]", { include-group = "test-qt" }]
 dev = [
     { include-group = "test" },
@@ -131,7 +133,7 @@ line-length = 88
 target-version = "py39"
 src = ["src", "tests"]
 fix = true
-# unsafe-fixes = true
+unsafe-fixes = true
 
 [tool.ruff.lint]
 pydocstyle = { convention = "numpy" }
@@ -179,6 +181,7 @@ filterwarnings = [
     "ignore:Jupyter is migrating:DeprecationWarning",
     "ignore:The `ipykernel.comm.Comm` class has been deprecated",
     "ignore:.*read_binary is deprecated:",
+    "ignore::DeprecationWarning:matplotlib",
 ]
 
 # https://mypy.readthedocs.io/en/stable/config_file.html
diff --git a/src/magicgui/signature.py b/src/magicgui/signature.py
index d67ed6a..3a84cb8 100644
--- a/src/magicgui/signature.py
+++ b/src/magicgui/signature.py
@@ -182,14 +182,19 @@ class MagicParameter(inspect.Parameter):
         rep = rep.replace(": NoneType = ", "=")
         return rep
 
-    def __str__(self) -> str:
-        """Return string representation of the Parameter in a signature."""
+    def _format(self, *, quote_annotation_strings: bool = True) -> str:
+        """Return formatted string for use in Signature.format() (Python 3.14+)."""
         hint, _ = get_args(self.annotation)
-        return str(
-            inspect.Parameter(
-                self.name, self.kind, default=self.default, annotation=hint
-            )
+        param = inspect.Parameter(
+            self.name, self.kind, default=self.default, annotation=hint
         )
+        if hasattr(param, "_format"):  # python 3.14+
+            return param._format(quote_annotation_strings=quote_annotation_strings)  # type: ignore[no-any-return]
+        return str(param)
+
+    def __str__(self) -> str:
+        """Return string representation of the Parameter in a signature."""
+        return self._format()
 
     def to_widget(
         self,
diff --git a/tests/conftest.py b/tests/conftest.py
index b2ed66d..23c6a48 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,7 +1,19 @@
+import os
+
 import pytest
 
 from magicgui.application import use_app
 
+# Disable tqdm's TMonitor thread to prevent race conditions with Qt threading
+# that can cause intermittent segfaults on CI (especially with PySide6 on Linux).
+# See: https://github.com/tqdm/tqdm/issues/469
+try:
+    from tqdm import tqdm as _tqdm_std
+
+    _tqdm_std.monitor_interval = 0
+except ImportError:
+    pass
+
 
 @pytest.fixture(scope="session")
 def qapp():
