diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 949b108c60c4f6..137f54313c52a7 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 removed in + 3.15. + + .. 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 + removed in Python 3.15. To create a NamedTuple class with 0 fields, use + ``class NT(NamedTuple): ...`` or ``NT = NamedTuple("NT", [])``. + .. class:: NewType(name, tp) Helper class to create low-overhead :ref:`distinct types `. diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index e3090f1fb7f51a..e0d6e219bd5fa8 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -133,6 +133,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 + removed in Python 3.15. To create a NamedTuple class with 0 fields, use + ``class NT(NamedTuple): ...`` or ``NT = NamedTuple("NT", [])``. + (Contributed by Alex Waygood in :gh:`105566`.) Removed diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 432fc88b1c072e..4a6faa01f7d0af 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -7124,18 +7124,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)]) @@ -7145,12 +7174,24 @@ def test_namedtuple_special_keyword_names(self): self.assertEqual(a.fields, [('bar', tuple)]) def test_empty_namedtuple(self): - NT = NamedTuple('NT') + with self.assertWarnsRegex( + DeprecationWarning, + "Failing to pass a value for the 'fields' parameter is deprecated" + ): + NT1 = NamedTuple('NT1') + + with self.assertWarnsRegex( + DeprecationWarning, + "Passing `None` as the 'fields' parameter is deprecated" + ): + 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, {}) @@ -7160,13 +7201,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 a531e7d7abbef6..e86d8079019971 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2753,7 +2753,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() + + +def NamedTuple(typename, fields=_sentinel, /, **kwargs): """Typed version of namedtuple. Usage:: @@ -2773,11 +2782,40 @@ 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" + deprecation_msg = ( + "{name} is deprecated and will be disallowed in Python {remove}. " + "To create an empty NamedTuple using functional syntax, " + "pass an empty list, e.g. `NT = NamedTuple('NT', [])`." + ) + 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" + deprecation_msg = ( + "{name} is deprecated and will be disallowed in Python {remove}. " + "To create an empty NamedTuple using functional syntax, " + "pass an empty list, e.g. `NT = NamedTuple('NT', [])`." + ) 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 00000000000000..0a32f60fc9163f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-06-09-20-34-23.gh-issue-105566.YxlGg1.rst @@ -0,0 +1,8 @@ +Deprecate creating a :class:`typing.NamedTuple` class using keyword +arguments to denote the fields (``NT = NamedTuple("NT", x=int, y=str)``). +Use the class-based syntax or the functional syntax instead. + +Two methods of creating ``NamedTuple``\s with 0 fields using the functional +syntax are also deprecated: ``NT = NamedTuple("NT")`` and ``NT = +NamedTuple("NT", None)``. To create a ``NamedTuple`` class with 0 fields, +either use ``class NT(NamedTuple): ...`` or ``NT = NamedTuple("NT", [])``.