From be88d696b1c86413feb14346dccd55e039169278 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 9 Jun 2023 22:27:27 +0100 Subject: [PATCH 1/3] gh-105566: Deprecate unusual ways of constructing `typing.NamedTuple`s --- Doc/library/typing.rst | 13 ++++ Doc/whatsnew/3.13.rst | 11 +++ Lib/test/test_typing.py | 75 ++++++++++++++++--- Lib/typing.py | 44 ++++++++++- ...-06-09-20-34-23.gh-issue-105566.YxlGg1.rst | 8 ++ 5 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-06-09-20-34-23.gh-issue-105566.YxlGg1.rst 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", [])``. From 9bb0a18144de59b1355adb21f676908df4abac7d Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 11 Jun 2023 16:42:26 +0100 Subject: [PATCH 2/3] Improve docs and error message --- Doc/library/typing.rst | 8 +++---- Doc/whatsnew/3.13.rst | 2 +- Lib/test/test_typing.py | 24 ++++++++++++------- Lib/typing.py | 16 ++++++++----- ...-06-09-20-34-23.gh-issue-105566.YxlGg1.rst | 10 ++++---- 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 137f54313c52a7..c7f51029befc68 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -2040,16 +2040,16 @@ These are not used in annotations. They are building blocks for declaring types. .. 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. + (``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 - removed in Python 3.15. To create a NamedTuple class with 0 fields, use - ``class NT(NamedTuple): ...`` or ``NT = NamedTuple("NT", [])``. + disallowed in Python 3.15. To create a NamedTuple class with 0 fields, + use ``class NT(NamedTuple): ...`` or ``NT = NamedTuple("NT", [])``. .. class:: NewType(name, tp) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index e0d6e219bd5fa8..75f422fdd0c5de 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -141,7 +141,7 @@ Deprecated 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 + disallowed 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`.) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 4a6faa01f7d0af..b85a13e4485124 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -7174,16 +7174,24 @@ def test_namedtuple_special_keyword_names(self): self.assertEqual(a.fields, [('bar', tuple)]) def test_empty_namedtuple(self): - with self.assertWarnsRegex( - DeprecationWarning, - "Failing to pass a value for the 'fields' parameter is deprecated" - ): + 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') - with self.assertWarnsRegex( - DeprecationWarning, - "Passing `None` as the 'fields' parameter is deprecated" - ): + 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', []) diff --git a/Lib/typing.py b/Lib/typing.py index e86d8079019971..b0468e762cdd47 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2791,11 +2791,13 @@ class Employee(NamedTuple): ) 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 an empty NamedTuple using functional syntax, " - "pass an empty list, e.g. `NT = NamedTuple('NT', [])`." - ) + "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( @@ -2804,11 +2806,13 @@ class Employee(NamedTuple): ) 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 an empty NamedTuple using functional syntax, " - "pass an empty list, e.g. `NT = NamedTuple('NT', [])`." - ) + "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") 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 index 0a32f60fc9163f..8811f535dc01f2 100644 --- 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 @@ -1,8 +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``\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", [])``. +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): ...`` or +``NT = NamedTuple("NT", [])``. From 77b66dc3f19ebcabb3fba0e2b176381b0358eb14 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 14 Jun 2023 08:58:04 +0100 Subject: [PATCH 3/3] pass --- Doc/library/typing.rst | 2 +- Doc/whatsnew/3.13.rst | 2 +- .../next/Library/2023-06-09-20-34-23.gh-issue-105566.YxlGg1.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index c7f51029befc68..c1bf18a5843b82 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -2049,7 +2049,7 @@ These are not used in annotations. They are building blocks for declaring types. 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): ...`` or ``NT = NamedTuple("NT", [])``. + use ``class NT(NamedTuple): pass`` or ``NT = NamedTuple("NT", [])``. .. class:: NewType(name, tp) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 75f422fdd0c5de..fe464efe938e25 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -142,7 +142,7 @@ Deprecated (``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): ...`` or ``NT = NamedTuple("NT", [])``. + ``class NT(NamedTuple): pass`` or ``NT = NamedTuple("NT", [])``. (Contributed by Alex Waygood in :gh:`105566`.) 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 index 8811f535dc01f2..c2c497aee513d3 100644 --- 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 @@ -6,5 +6,5 @@ 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): ...`` or +``NamedTuple`` class with 0 fields, either use ``class NT(NamedTuple): pass`` or ``NT = NamedTuple("NT", [])``.