]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
šŸ› Fix TYPE_CHECKING annotations for Python 3.14 (PEP 649) (#14789)
authorMickaƫl GuƩrin <mickael.guerin@getalma.eu>
Wed, 4 Feb 2026 13:49:44 +0000 (14:49 +0100)
committerGitHub <noreply@github.com>
Wed, 4 Feb 2026 13:49:44 +0000 (14:49 +0100)
fastapi/dependencies/utils.py
tests/test_stringified_annotation_dependency_py314.py [new file with mode: 0644]
tests/test_tutorial/test_dependencies/test_tutorial008.py
tests/utils.py

index b647818c4b6fd6b45b3f25f3c75b50ce5f4e8c90..fc5dfed85a1036211cef93641c44f4c8297eeac5 100644 (file)
@@ -204,7 +204,12 @@ def _get_signature(call: Callable[..., Any]) -> inspect.Signature:
         except NameError:
             # Handle type annotations with if TYPE_CHECKING, not used by FastAPI
             # e.g. dependency return types
-            signature = inspect.signature(call)
+            if sys.version_info >= (3, 14):
+                from annotationlib import Format
+
+                signature = inspect.signature(call, annotation_format=Format.FORWARDREF)
+            else:
+                signature = inspect.signature(call)
     else:
         signature = inspect.signature(call)
     return signature
diff --git a/tests/test_stringified_annotation_dependency_py314.py b/tests/test_stringified_annotation_dependency_py314.py
new file mode 100644 (file)
index 0000000..da9b429
--- /dev/null
@@ -0,0 +1,30 @@
+from typing import TYPE_CHECKING, Annotated
+
+from fastapi import Depends, FastAPI
+from fastapi.testclient import TestClient
+
+from .utils import needs_py314
+
+if TYPE_CHECKING:  # pragma: no cover
+
+    class DummyUser: ...
+
+
+@needs_py314
+def test_stringified_annotation():
+    # python3.14: Use forward reference without "from __future__ import annotations"
+    async def get_current_user() -> DummyUser | None:
+        return None
+
+    app = FastAPI()
+
+    client = TestClient(app)
+
+    @app.get("/")
+    async def get(
+        current_user: Annotated[DummyUser | None, Depends(get_current_user)],
+    ) -> str:
+        return "hello world"
+
+    response = client.get("/")
+    assert response.status_code == 200
index 9d7377ebe4d1e57cf6c0e9e4a9457e0ab85815d0..5a2d226bffb557e61ddb36af0977dde677d5f983 100644 (file)
@@ -1,4 +1,5 @@
 import importlib
+import sys
 from types import ModuleType
 from typing import Annotated, Any
 from unittest.mock import Mock, patch
@@ -12,8 +13,13 @@ from fastapi.testclient import TestClient
     name="module",
     params=[
         "tutorial008_py39",
-        # Fails with `NameError: name 'DepA' is not defined`
-        pytest.param("tutorial008_an_py39", marks=pytest.mark.xfail),
+        pytest.param(
+            "tutorial008_an_py39",
+            marks=pytest.mark.xfail(
+                sys.version_info < (3, 14),
+                reason="Fails with `NameError: name 'DepA' is not defined`",
+            ),
+        ),
     ],
 )
 def get_module(request: pytest.FixtureRequest):
index efa0bfd52b0e70dd4c47e09e919d1b915a97045f..4cbfee79f5d383a180a6a8b8fd5c181239e860ea 100644 (file)
@@ -6,8 +6,8 @@ needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires pyth
 needs_py310 = pytest.mark.skipif(
     sys.version_info < (3, 10), reason="requires python3.10+"
 )
-needs_py_lt_314 = pytest.mark.skipif(
-    sys.version_info >= (3, 14), reason="requires python3.13-"
+needs_py314 = pytest.mark.skipif(
+    sys.version_info < (3, 14), reason="requires python3.14+"
 )