]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-105570: Deprecate unusual ways of creating empty TypedDicts (#105780)
authorAlex Waygood <Alex.Waygood@Gmail.com>
Wed, 14 Jun 2023 14:58:41 +0000 (15:58 +0100)
committerGitHub <noreply@github.com>
Wed, 14 Jun 2023 14:58:41 +0000 (14:58 +0000)
Deprecate two methods of creating typing.TypedDict classes with 0 fields using the functional syntax: `TD = TypedDict("TD")` and `TD = TypedDict("TD", None)`. Both will be disallowed in Python 3.15. To create a TypedDict class with 0 fields, either use `class TD(TypedDict): pass` or `TD = TypedDict("TD", {})`.

Doc/library/typing.rst
Doc/whatsnew/3.13.rst
Lib/test/test_typing.py
Lib/typing.py
Misc/NEWS.d/next/Library/2023-06-14-14-32-31.gh-issue-105570.sFTtQU.rst [new file with mode: 0644]

index 31861a0d613a88237317f6d05fe6300a33102ba5..73555d54115f3a838e06fa5810d45373935f2431 100644 (file)
@@ -2388,6 +2388,14 @@ These are not used in annotations. They are building blocks for declaring types.
    .. versionchanged:: 3.13
       Removed support for the keyword-argument method of creating ``TypedDict``\ s.
 
+   .. deprecated-removed:: 3.13 3.15
+      When using the functional syntax to create a TypedDict class, failing to
+      pass a value to the 'fields' parameter (``TD = TypedDict("TD")``) is
+      deprecated. Passing ``None`` to the 'fields' parameter
+      (``TD = TypedDict("TD", None)``) is also deprecated. Both will be
+      disallowed in Python 3.15. To create a TypedDict class with 0 fields,
+      use ``class TD(TypedDict): pass`` or ``TD = TypedDict("TD", {})``.
+
 Protocols
 ---------
 
index cf7c2ca24429d64421dcb91ed426222b9bb1c853..7f61ade808cce53f147af9a97cbb6db5b38fb61c 100644 (file)
@@ -146,12 +146,15 @@ Deprecated
   be disallowed in Python 3.15. Use the class-based syntax or the functional
   syntax instead. (Contributed by Alex Waygood in :gh:`105566`.)
 * When using the functional syntax to create a :class:`typing.NamedTuple`
-  class, failing to pass a value to the 'fields' parameter
-  (``NT = NamedTuple("NT")``) is deprecated. Passing ``None`` to the 'fields'
-  parameter (``NT = NamedTuple("NT", None)``) is also deprecated. Both will be
-  disallowed in Python 3.15. To create a NamedTuple class with 0 fields, use
-  ``class NT(NamedTuple): pass`` or ``NT = NamedTuple("NT", [])``.
-  (Contributed by Alex Waygood in :gh:`105566`.)
+  class or a :class:`typing.TypedDict` class, failing to pass a value to the
+  'fields' parameter (``NT = NamedTuple("NT")`` or ``TD = TypedDict("TD")``) is
+  deprecated. Passing ``None`` to the 'fields' parameter
+  (``NT = NamedTuple("NT", None)`` or ``TD = TypedDict("TD", None)``) is also
+  deprecated. Both will be disallowed in Python 3.15. To create a NamedTuple
+  class with 0 fields, use ``class NT(NamedTuple): pass`` or
+  ``NT = NamedTuple("NT", [])``. To create a TypedDict class with 0 fields, use
+  ``class TD(TypedDict): pass`` or ``TD = TypedDict("TD", {})``.
+  (Contributed by Alex Waygood in :gh:`105566` and :gh:`105570`.)
 
 * :mod:`array`'s ``'u'`` format code, deprecated in docs since Python 3.3,
   emits :exc:`DeprecationWarning` since 3.13
index 92f38043af5c0391516cacc1ccff18bd1ba6229e..3eb0fcad69e5e5becffad385981d81819db37411 100644 (file)
@@ -7823,6 +7823,40 @@ class TypedDictTests(BaseTestCase):
         self.assertEqual(MultipleGenericBases.__orig_bases__, (GenericParent[int], GenericParent[float]))
         self.assertEqual(CallTypedDict.__orig_bases__, (TypedDict,))
 
+    def test_zero_fields_typeddicts(self):
+        T1 = TypedDict("T1", {})
+        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:
+            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)
+
 
 class RequiredTests(BaseTestCase):
 
index 570cb80cfeeb2c9138709d7a26244836c78feea8..1dd9398344639b9aa039a7a0e12ed90c1397bb11 100644 (file)
@@ -2908,7 +2908,7 @@ class _TypedDictMeta(type):
     __instancecheck__ = __subclasscheck__
 
 
-def TypedDict(typename, fields=None, /, *, total=True):
+def TypedDict(typename, fields=_sentinel, /, *, 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
@@ -2955,7 +2955,22 @@ def TypedDict(typename, fields=None, /, *, total=True):
 
     See PEP 655 for more details on Required and NotRequired.
     """
-    if fields is None:
+    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)}
diff --git a/Misc/NEWS.d/next/Library/2023-06-14-14-32-31.gh-issue-105570.sFTtQU.rst b/Misc/NEWS.d/next/Library/2023-06-14-14-32-31.gh-issue-105570.sFTtQU.rst
new file mode 100644 (file)
index 0000000..e31a8ee
--- /dev/null
@@ -0,0 +1,5 @@
+Deprecate two methods of creating :class:`typing.TypedDict` classes with 0
+fields using the functional syntax: ``TD = TypedDict("TD")`` and
+``TD = TypedDict("TD", None)``. Both will be disallowed in Python 3.15. To create a
+``TypedDict`` class with 0 fields, either use ``class TD(TypedDict): pass``
+or ``TD = TypedDict("TD", {})``.