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", [])`.
This commit is contained in:
Alex Waygood 2023-06-14 13:38:49 +01:00 committed by GitHub
parent fc8037d84c
commit ad56340b66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 153 additions and 12 deletions

View File

@ -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>`.

View File

@ -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

View File

@ -7189,18 +7189,47 @@ class NamedTupleTests(BaseTestCase):
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 @@ class NamedTupleTests(BaseTestCase):
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 NamedTupleTests(BaseTestCase):
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):

View File

@ -2755,7 +2755,16 @@ class NamedTupleMeta(type):
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 @@ def NamedTuple(typename, fields=None, /, **kwargs):
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

View File

@ -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", [])``.