]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-133823: require explicit empty sequence for 0-field `TypedDict` objects (#133863)
authorBénédikt Tran <10796600+picnixz@users.noreply.github.com>
Sun, 11 May 2025 08:04:45 +0000 (10:04 +0200)
committerGitHub <noreply@github.com>
Sun, 11 May 2025 08:04:45 +0000 (08:04 +0000)
Doc/whatsnew/3.15.rst
Lib/test/test_typing.py
Lib/typing.py
Misc/NEWS.d/next/Library/2025-05-11-08-48-55.gh-issue-133823.F8udQy.rst [new file with mode: 0644]

index b543f71a44d5b3435d1a055671fb2d42924fdfa6..5a9bf1ae3c97bf091d8b6e19aa1e98a7c7efd28b 100644 (file)
@@ -137,6 +137,12 @@ typing
   Use the class-based syntax or the functional syntax instead.
   (Contributed by Bénédikt Tran in :gh:`133817`.)
 
+* Using ``TD = TypedDict("TD")`` or ``TD = TypedDict("TD", None)`` to
+  construct a :class:`~typing.TypedDict` type with zero field is no
+  longer supported. Use ``class TD(TypedDict): pass``
+  or ``TD = TypedDict("TD", {})`` instead.
+  (Contributed by Bénédikt Tran in :gh:`133823`.)
+
 
 Porting to Python 3.15
 ======================
index f4d75c4376f0a1d0fae7cc0f7e2eeb90a54d9d18..bc03e6bf2d796257948796dfe6c9ab7e4be0366e 100644 (file)
@@ -8853,39 +8853,27 @@ class TypedDictTests(BaseTestCase):
         self.assertEqual(CallTypedDict.__orig_bases__, (TypedDict,))
 
     def test_zero_fields_typeddicts(self):
-        T1 = TypedDict("T1", {})
+        T1a = TypedDict("T1a", {})
+        T1b = TypedDict("T1b", [])
+        T1c = TypedDict("T1c", ())
         class T2(TypedDict): pass
         class T3[tvar](TypedDict): pass
         S = TypeVar("S")
         class T4(TypedDict, Generic[S]): pass
 
-        expected_warning = re.escape(
-            "Failing to pass a value for the 'fields' parameter is deprecated "
-            "and will be disallowed in Python 3.15. "
-            "To create a TypedDict class with 0 fields "
-            "using the functional syntax, "
-            "pass an empty dictionary, e.g. `T5 = TypedDict('T5', {})`."
-        )
-        with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"):
-            T5 = TypedDict('T5')
-
-        expected_warning = re.escape(
-            "Passing `None` as the 'fields' parameter is deprecated "
-            "and will be disallowed in Python 3.15. "
-            "To create a TypedDict class with 0 fields "
-            "using the functional syntax, "
-            "pass an empty dictionary, e.g. `T6 = TypedDict('T6', {})`."
-        )
-        with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"):
-            T6 = TypedDict('T6', None)
-
-        for klass in T1, T2, T3, T4, T5, T6:
+        for klass in T1a, T1b, T1c, T2, T3, T4:
             with self.subTest(klass=klass.__name__):
                 self.assertEqual(klass.__annotations__, {})
                 self.assertEqual(klass.__required_keys__, set())
                 self.assertEqual(klass.__optional_keys__, set())
                 self.assertIsInstance(klass(), dict)
 
+    def test_errors(self):
+        with self.assertRaisesRegex(TypeError, "missing 1 required.*argument"):
+            TypedDict('TD')
+        with self.assertRaisesRegex(TypeError, "object is not iterable"):
+            TypedDict('TD', None)
+
     def test_readonly_inheritance(self):
         class Base1(TypedDict):
             a: ReadOnly[int]
index d4c808050b35bc0d7c2a0290d27bb622ceae21d5..44f39e9672f46b0e2ceff02ce40163e4d26dba14 100644 (file)
@@ -3159,7 +3159,7 @@ class _TypedDictMeta(type):
     __instancecheck__ = __subclasscheck__
 
 
-def TypedDict(typename, fields=_sentinel, /, *, total=True):
+def TypedDict(typename, fields, /, *, total=True):
     """A simple typed namespace. At runtime it is equivalent to a plain dict.
 
     TypedDict creates a dictionary type such that a type checker will expect all
@@ -3214,24 +3214,6 @@ def TypedDict(typename, fields=_sentinel, /, *, total=True):
             username: str      # the "username" key can be changed
 
     """
-    if fields is _sentinel or fields is None:
-        import warnings
-
-        if fields is _sentinel:
-            deprecated_thing = "Failing to pass a value for the 'fields' parameter"
-        else:
-            deprecated_thing = "Passing `None` as the 'fields' parameter"
-
-        example = f"`{typename} = TypedDict({typename!r}, {{{{}}}})`"
-        deprecation_msg = (
-            "{name} is deprecated and will be disallowed in Python {remove}. "
-            "To create a TypedDict class with 0 fields "
-            "using the functional syntax, "
-            "pass an empty dictionary, e.g. "
-        ) + example + "."
-        warnings._deprecated(deprecated_thing, message=deprecation_msg, remove=(3, 15))
-        fields = {}
-
     ns = {'__annotations__': dict(fields)}
     module = _caller()
     if module is not None:
diff --git a/Misc/NEWS.d/next/Library/2025-05-11-08-48-55.gh-issue-133823.F8udQy.rst b/Misc/NEWS.d/next/Library/2025-05-11-08-48-55.gh-issue-133823.F8udQy.rst
new file mode 100644 (file)
index 0000000..67b44ac
--- /dev/null
@@ -0,0 +1,3 @@
+Remove support for ``TD = TypedDict("TD")`` and ``TD = TypedDict("TD", None)``
+calls for constructing :class:`typing.TypedDict` objects with zero field.
+Patch by Bénédikt Tran.