[Python-checkins] gh-105566: Deprecate unusual ways of creating `typing.NamedTuple` classes (#105609)

AlexWaygood webhook-mailer at python.org
Wed Jun 14 08:38:56 EDT 2023


https://github.com/python/cpython/commit/ad56340b665c5d8ac1f318964f71697bba41acb7
commit: ad56340b665c5d8ac1f318964f71697bba41acb7
branch: main
author: Alex Waygood <Alex.Waygood at Gmail.com>
committer: AlexWaygood <Alex.Waygood at Gmail.com>
date: 2023-06-14T13:38:49+01:00
summary:

gh-105566: Deprecate unusual ways of creating `typing.NamedTuple` classes (#105609)

Deprecate creating a typing.NamedTuple class using keyword arguments to denote the fields (`NT = NamedTuple("NT", x=int, y=str)`). This will be disallowed in Python 3.15. Use the class-based syntax or the functional syntax instead.

Two methods of creating `NamedTuple` classes with 0 fields using the functional syntax are also deprecated, and will be disallowed in Python 3.15: `NT = NamedTuple("NT")` and `NT = NamedTuple("NT", None)`. To create a `NamedTuple` class with 0 fields, either use `class NT(NamedTuple): pass` or `NT = NamedTuple("NT", [])`.

files:
A Misc/NEWS.d/next/Library/2023-06-09-20-34-23.gh-issue-105566.YxlGg1.rst
M Doc/library/typing.rst
M Doc/whatsnew/3.13.rst
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index 487be8f28a78..aedef091e44c 100644
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -2038,6 +2038,19 @@ These are not used in annotations. They are building blocks for declaring types.
    .. versionchanged:: 3.11
       Added support for generic namedtuples.
 
+   .. deprecated-removed:: 3.13 3.15
+      The undocumented keyword argument syntax for creating NamedTuple classes
+      (``NT = NamedTuple("NT", x=int)``) is deprecated, and will be disallowed
+      in 3.15. Use the class-based syntax or the functional syntax instead.
+
+   .. deprecated-removed:: 3.13 3.15
+      When using the functional syntax to create a 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", [])``.
+
 .. class:: NewType(name, tp)
 
    Helper class to create low-overhead :ref:`distinct types <distinct>`.
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index fcd10e522c8a..cf7c2ca24429 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -141,6 +141,17 @@ Deprecated
   methods of the :class:`wave.Wave_read` and :class:`wave.Wave_write` classes.
   They will be removed in Python 3.15.
   (Contributed by Victor Stinner in :gh:`105096`.)
+* Creating a :class:`typing.NamedTuple` class using keyword arguments to denote
+  the fields (``NT = NamedTuple("NT", x=int, y=int)``) is deprecated, and will
+  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`.)
 
 * :mod:`array`'s ``'u'`` format code, deprecated in docs since Python 3.3,
   emits :exc:`DeprecationWarning` since 3.13
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index a36d801c5251..92f38043af5c 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -7189,18 +7189,47 @@ class Group(NamedTuple):
         self.assertEqual(a, (1, [2]))
 
     def test_namedtuple_keyword_usage(self):
-        LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int)
+        with self.assertWarnsRegex(
+            DeprecationWarning,
+            "Creating NamedTuple classes using keyword arguments is deprecated"
+        ):
+            LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int)
+
         nick = LocalEmployee('Nick', 25)
         self.assertIsInstance(nick, tuple)
         self.assertEqual(nick.name, 'Nick')
         self.assertEqual(LocalEmployee.__name__, 'LocalEmployee')
         self.assertEqual(LocalEmployee._fields, ('name', 'age'))
         self.assertEqual(LocalEmployee.__annotations__, dict(name=str, age=int))
-        with self.assertRaises(TypeError):
+
+        with self.assertRaisesRegex(
+            TypeError,
+            "Either list of fields or keywords can be provided to NamedTuple, not both"
+        ):
             NamedTuple('Name', [('x', int)], y=str)
 
+        with self.assertRaisesRegex(
+            TypeError,
+            "Either list of fields or keywords can be provided to NamedTuple, not both"
+        ):
+            NamedTuple('Name', [], y=str)
+
+        with self.assertRaisesRegex(
+            TypeError,
+            (
+                r"Cannot pass `None` as the 'fields' parameter "
+                r"and also specify fields using keyword arguments"
+            )
+        ):
+            NamedTuple('Name', None, x=int)
+
     def test_namedtuple_special_keyword_names(self):
-        NT = NamedTuple("NT", cls=type, self=object, typename=str, fields=list)
+        with self.assertWarnsRegex(
+            DeprecationWarning,
+            "Creating NamedTuple classes using keyword arguments is deprecated"
+        ):
+            NT = NamedTuple("NT", cls=type, self=object, typename=str, fields=list)
+
         self.assertEqual(NT.__name__, 'NT')
         self.assertEqual(NT._fields, ('cls', 'self', 'typename', 'fields'))
         a = NT(cls=str, self=42, typename='foo', fields=[('bar', tuple)])
@@ -7210,12 +7239,32 @@ def test_namedtuple_special_keyword_names(self):
         self.assertEqual(a.fields, [('bar', tuple)])
 
     def test_empty_namedtuple(self):
-        NT = NamedTuple('NT')
+        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 NamedTuple class with 0 fields "
+            "using the functional syntax, "
+            "pass an empty list, e.g. `NT1 = NamedTuple('NT1', [])`."
+        )
+        with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"):
+            NT1 = NamedTuple('NT1')
+
+        expected_warning = re.escape(
+            "Passing `None` as the 'fields' parameter is deprecated "
+            "and will be disallowed in Python 3.15. "
+            "To create a NamedTuple class with 0 fields "
+            "using the functional syntax, "
+            "pass an empty list, e.g. `NT2 = NamedTuple('NT2', [])`."
+        )
+        with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"):
+            NT2 = NamedTuple('NT2', None)
+
+        NT3 = NamedTuple('NT2', [])
 
         class CNT(NamedTuple):
             pass  # empty body
 
-        for struct in [NT, CNT]:
+        for struct in NT1, NT2, NT3, CNT:
             with self.subTest(struct=struct):
                 self.assertEqual(struct._fields, ())
                 self.assertEqual(struct._field_defaults, {})
@@ -7225,13 +7274,29 @@ class CNT(NamedTuple):
     def test_namedtuple_errors(self):
         with self.assertRaises(TypeError):
             NamedTuple.__new__()
-        with self.assertRaises(TypeError):
+
+        with self.assertRaisesRegex(
+            TypeError,
+            "missing 1 required positional argument"
+        ):
             NamedTuple()
-        with self.assertRaises(TypeError):
+
+        with self.assertRaisesRegex(
+            TypeError,
+            "takes from 1 to 2 positional arguments but 3 were given"
+        ):
             NamedTuple('Emp', [('name', str)], None)
-        with self.assertRaises(ValueError):
+
+        with self.assertRaisesRegex(
+            ValueError,
+            "Field names cannot start with an underscore"
+        ):
             NamedTuple('Emp', [('_name', str)])
-        with self.assertRaises(TypeError):
+
+        with self.assertRaisesRegex(
+            TypeError,
+            "missing 1 required positional argument: 'typename'"
+        ):
             NamedTuple(typename='Emp', name=str, id=int)
 
     def test_copy_and_pickle(self):
diff --git a/Lib/typing.py b/Lib/typing.py
index 4e6dc4477353..570cb80cfeeb 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -2755,7 +2755,16 @@ def __new__(cls, typename, bases, ns):
         return nm_tpl
 
 
-def NamedTuple(typename, fields=None, /, **kwargs):
+class _Sentinel:
+    __slots__ = ()
+    def __repr__(self):
+        return '<sentinel>'
+
+
+_sentinel = _Sentinel()
+
+
+def NamedTuple(typename, fields=_sentinel, /, **kwargs):
     """Typed version of namedtuple.
 
     Usage::
@@ -2775,11 +2784,44 @@ class Employee(NamedTuple):
 
         Employee = NamedTuple('Employee', [('name', str), ('id', int)])
     """
-    if fields is None:
-        fields = kwargs.items()
+    if fields is _sentinel:
+        if kwargs:
+            deprecated_thing = "Creating NamedTuple classes using keyword arguments"
+            deprecation_msg = (
+                "{name} is deprecated and will be disallowed in Python {remove}. "
+                "Use the class-based or functional syntax instead."
+            )
+        else:
+            deprecated_thing = "Failing to pass a value for the 'fields' parameter"
+            example = f"`{typename} = NamedTuple({typename!r}, [])`"
+            deprecation_msg = (
+                "{name} is deprecated and will be disallowed in Python {remove}. "
+                "To create a NamedTuple class with 0 fields "
+                "using the functional syntax, "
+                "pass an empty list, e.g. "
+            ) + example + "."
+    elif fields is None:
+        if kwargs:
+            raise TypeError(
+                "Cannot pass `None` as the 'fields' parameter "
+                "and also specify fields using keyword arguments"
+            )
+        else:
+            deprecated_thing = "Passing `None` as the 'fields' parameter"
+            example = f"`{typename} = NamedTuple({typename!r}, [])`"
+            deprecation_msg = (
+                "{name} is deprecated and will be disallowed in Python {remove}. "
+                "To create a NamedTuple class with 0 fields "
+                "using the functional syntax, "
+                "pass an empty list, e.g. "
+            ) + example + "."
     elif kwargs:
         raise TypeError("Either list of fields or keywords"
                         " can be provided to NamedTuple, not both")
+    if fields is _sentinel or fields is None:
+        import warnings
+        warnings._deprecated(deprecated_thing, message=deprecation_msg, remove=(3, 15))
+        fields = kwargs.items()
     nt = _make_nmtuple(typename, fields, module=_caller())
     nt.__orig_bases__ = (NamedTuple,)
     return nt
diff --git a/Misc/NEWS.d/next/Library/2023-06-09-20-34-23.gh-issue-105566.YxlGg1.rst b/Misc/NEWS.d/next/Library/2023-06-09-20-34-23.gh-issue-105566.YxlGg1.rst
new file mode 100644
index 000000000000..c2c497aee513
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-06-09-20-34-23.gh-issue-105566.YxlGg1.rst
@@ -0,0 +1,10 @@
+Deprecate creating a :class:`typing.NamedTuple` class using keyword
+arguments to denote the fields (``NT = NamedTuple("NT", x=int, y=str)``).
+This will be disallowed in Python 3.15.
+Use the class-based syntax or the functional syntax instead.
+
+Two methods of creating ``NamedTuple`` classes with 0 fields using the
+functional syntax are also deprecated, and will be disallowed in Python 3.15:
+``NT = NamedTuple("NT")`` and ``NT = NamedTuple("NT", None)``. To create a
+``NamedTuple`` class with 0 fields, either use ``class NT(NamedTuple): pass`` or
+``NT = NamedTuple("NT", [])``.



More information about the Python-checkins mailing list